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..1588a326238 --- /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.2+ (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..04e9a4c81e6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,98 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + groups: + github_actions: + patterns: + - "*" + - package-ecosystem: bundler + directory: / + schedule: + interval: monthly + versioning-strategy: lockfile-only + groups: + rails_default: + patterns: + - "*" + - package-ecosystem: npm + directory: / + schedule: + interval: monthly + versioning-strategy: lockfile-only + groups: + npm: + patterns: + - "*" + - package-ecosystem: bundler + directory: /gemfiles/rails_70 + schedule: + interval: monthly + versioning-strategy: lockfile-only + groups: + rails_70: + patterns: + - "*" + ignore: + - dependency-name: rails + versions: ">= 7.1.0" + - dependency-name: rails-i18n + versions: ">= 8.0.0" + - dependency-name: railties + versions: ">= 7.1.0" + - dependency-name: rspec-rails + versions: ">= 8.0.0" + - dependency-name: sqlite3 + versions: ">= 2" + - package-ecosystem: bundler + directory: /gemfiles/rails_71 + schedule: + interval: monthly + versioning-strategy: lockfile-only + groups: + rails_71: + patterns: + - "*" + ignore: + - dependency-name: rails + versions: ">= 7.2.0" + - dependency-name: rails-i18n + versions: ">= 8.0.0" + - dependency-name: railties + versions: ">= 7.2.0" + - dependency-name: ransack + versions: ">= 4.4.1" + - dependency-name: rspec-rails + versions: ">= 8.0.0" + - package-ecosystem: bundler + directory: /gemfiles/rails_72 + schedule: + interval: monthly + versioning-strategy: lockfile-only + groups: + rails_72: + patterns: + - "*" + ignore: + - 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 + schedule: + interval: monthly + versioning-strategy: lockfile-only + groups: + rails_80: + patterns: + - "*" + 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..f9769b1fd28 --- /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: ${{ false && (github.event_name == 'pull_request' || github.repository_owner == 'activeadmin') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: tj-actions/changed-files@v47 + 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..bc843fdc7c3 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,123 @@ +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" + - "3.2" + os: + - ubuntu-latest + rails: + - rails_81 + - rails_80 + - rails_72 + exclude: + - ruby: '4.0' + os: ubuntu-latest + rails: rails_72 + steps: + - uses: actions/checkout@v6 + - 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@v5 + 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@v5 + 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@v6 + 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@v6 + - uses: actions/download-artifact@v7 + with: + path: coverage + pattern: coverage-ruby-* + merge-multiple: true + - uses: codecov/codecov-action@v5 + 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@v6 + - uses: tj-actions/changed-files@v47 + id: changed-files + with: + files: | + docs/** + package*.json + yarn.lock + - uses: actions/setup-node@v6 + if: steps.changed-files.outputs.any_changed == 'true' + with: + node-version: 24 + cache: yarn + - run: yarn install + if: steps.changed-files.outputs.any_changed == 'true' + - run: yarn docs:build + 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..6ff34de3ec3 --- /dev/null +++ b/.github/workflows/docs-deployment.yml @@ -0,0 +1,47 @@ +name: Docs Deployment + +on: + 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@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: yarn + - uses: actions/configure-pages@v5 + - run: yarn install + - run: yarn docs:build + - uses: actions/upload-pages-artifact@v4 + 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@v4 + id: deployment diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml new file mode 100644 index 00000000000..2ca53fa0981 --- /dev/null +++ b/.github/workflows/eslint.yml @@ -0,0 +1,33 @@ +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@v6 + - uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: yarn + - uses: tj-actions/changed-files@v47 + id: changed-files + with: + files: | + **.js + package*.json + yarn.lock + .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..074bbce9a68 --- /dev/null +++ b/.github/workflows/gherkin-lint.yml @@ -0,0 +1,32 @@ +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@v6 + - uses: tj-actions/changed-files@v47 + id: changed-files + with: + files: | + **.feature + .gherkin-lintrc + package*.json + yarn.lock + .github/workflows/gherkin-lint.yml + - uses: actions/setup-node@v6 + if: steps.changed-files.outputs.any_changed == 'true' + with: + node-version: ${{ env.NODE_VERSION }} + cache: yarn + - run: yarn install --frozen-lockfile --immutable + if: steps.changed-files.outputs.any_changed == 'true' + - run: yarn 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..0366407f5f4 --- /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@v6 + - uses: tj-actions/changed-files@v47 + 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..225f8815c93 --- /dev/null +++ b/.github/workflows/markdown-lint.yml @@ -0,0 +1,29 @@ +name: Markdown Lint + +on: + pull_request: + +env: + MARKDOWNLINT_FLAGS: ${{ vars.REVIEWDOG_MARKDOWNLINT_FLAGS || '--git-recurse .' }} + +jobs: + markdownlint: + name: Run markdownlint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: tj-actions/changed-files@v47 + 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..972785ac697 --- /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@v6 + - uses: tj-actions/changed-files@v47 + 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..9b90ee5e715 --- /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@v6 + - 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..8428283177c --- /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@v6 + - uses: tj-actions/changed-files@v47 + 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 beb2a8e0d7f..e26ea760672 100644 --- a/.gitignore +++ b/.gitignore @@ -1,49 +1,15 @@ -## Mac -.DS_Store - -## Windows -.Thumbs.db - -## TextMate -*.tm_project -*.tmproj -tmtags - -## Emacs -*~ -\#* -.\#* - -## Vim -*.swp -# IDEA / RUBYMINE -.idea - -## RVM -.rvmrc -.ruby-version -.ruby-gemset - -## Project (general) -tags -coverage -rdoc -doc -.yardoc -pkg - -## Project (specific) -bin/ +/tmp +/tasks/tmp +/coverage +/.yardoc +/.ruby-version +/pkg .bundle -spec/rails -*.sqlite3-journal -Gemfile.lock -Gemfile-*.lock -capybara* -viewcumber -test-rails* -public -.rspec -.rails-version -.rbenv-version -.localeapp/* +/.rspec_failures +/node_modules +/src +/vendor/bundle +/rails_70 +/dist +docs/.vitepress/cache +docs/.vitepress/dist diff --git a/.hound.yml b/.hound.yml deleted file mode 100644 index 59e5ffe2967..00000000000 --- a/.hound.yml +++ /dev/null @@ -1,14 +0,0 @@ -ruby: - enabled: false - -coffee_script: - enabled: false - -java_script: - enabled: false - -scss: - enabled: false - -Metrics/LineLength: - Max: 110 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..52b4296f1b3 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,423 @@ +--- + +inherit_mode: + merge: + - Include + +plugins: + - rubocop-capybara + - rubocop-packaging + - rubocop-performance + - rubocop-rails + - rubocop-rspec + +AllCops: + DisabledByDefault: true + TargetRubyVersion: 3.2 + TargetRailsVersion: 7.2 + + Exclude: + - .git/**/* + - .github/**/* + - bin/**/* + - gemfiles/**/vendor/**/* + - node_modules/**/* + - tmp/**/* + - vendor/**/* + + Include: + - gemfiles/*/Gemfile + - .simplecov + + DisplayCopNames: true + + StyleGuideCopsOnly: false + +Capybara: + Enabled: true + +Capybara/ClickLinkOrButtonStyle: + Enabled: true + +Capybara/CurrentPathExpectation: + Enabled: true + +Capybara/FindAllFirst: + Enabled: true + +Capybara/MatchStyle: + Enabled: true + +Capybara/NegationMatcher: + Enabled: true + +Capybara/NegationMatcherAfterVisit: + Enabled: true + +Capybara/RedundantWithinFind: + Enabled: true + +Capybara/RSpec/HaveSelector: + Enabled: true + +Capybara/RSpec/PredicateMatcher: + Enabled: true + +Capybara/SpecificActions: + Enabled: true + +Capybara/SpecificFinders: + Enabled: true + +Capybara/SpecificMatcher: + Enabled: true + +Capybara/VisibilityMatcher: + Enabled: true + +Layout/EndAlignment: + Enabled: true + +Layout/HashAlignment: + 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/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/ParameterAlignment: + Enabled: true + EnforcedStyle: with_fixed_indentation + +Layout/IndentationStyle: + Enabled: true + EnforcedStyle: spaces + +Lint/AmbiguousOperator: + Enabled: true + +Lint/AmbiguousRegexpLiteral: + Enabled: true + +Lint/ParenthesesAsGroupedExpression: + Enabled: true + +Lint/RedundantStringCoercion: + Enabled: true + +Lint/UselessAccessModifier: + Enabled: true + +Lint/UselessAssignment: + Enabled: true + +Packaging/BundlerSetupInTests: + Enabled: true + +Packaging/GemspecGit: + Enabled: true + +Packaging/RequireHardcodingLib: + Enabled: true + +Packaging/RequireRelativeHardcodingLib: + Enabled: true + +Performance: + 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/CaseWhenSplat: + Enabled: true + +Performance/Casecmp: + Enabled: false + +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/StringIdentifierArgument: + Enabled: true + +Performance/StringInclude: + Enabled: true + +Performance/StringReplacement: + Enabled: true + +Performance/StringBytesize: + 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 + +Layout/IndentationConsistency: + Enabled: true + +Layout/IndentationWidth: + Enabled: true + +Naming/PredicatePrefix: + Enabled: true + + ForbiddenPrefixes: + - is_ + - have_ + + AllowedMethods: + - has_many + - has_many_actions + +Style/StringLiterals: + Enabled: false + +Style/TrailingCommaInArguments: + Enabled: true + +Layout/TrailingEmptyLines: + Enabled: true + +Layout/TrailingWhitespace: + Enabled: true + +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 diff --git a/.simplecov b/.simplecov new file mode 100644 index 00000000000..154ddf432d9 --- /dev/null +++ b/.simplecov @@ -0,0 +1,12 @@ +# frozen_string_literal: true +SimpleCov.start do + add_filter %r{^/spec/} + add_filter "tmp/development_apps/" + add_filter "tmp/test_apps/" + add_filter "tasks/test_application.rb" +end + +if ENV["COVERAGE"] == "true" + require "simplecov-cobertura" + SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter +end diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4e7a7bcc028..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,44 +0,0 @@ -language: ruby -sudo: false -install: - - ./script/travis_cache download_bundle - - gem install bundler # use the very latest Bundler - - bundle install --without development --path=./bundle - - bundle clean # delete now-outdated gems - - ./script/travis_cache download_app - - bundle exec rake setup - - ./script/travis_cache upload -script: - - bundle exec rake test_with_coveralls -rvm: - - 1.9 - - 2.2.3 - - jruby-9.0.0.0 -env: - matrix: - - RAILS=3.2.22 - - RAILS=4.1.13 - - RAILS=4.2.4 - - RAILS=master - global: - - JRUBY_OPTS="-J-Xmx1024m --debug" -matrix: - fast_finish: true - exclude: - - rvm: 2.2.3 - env: RAILS=3.2.22 - - rvm: jruby-9.0.0.0 - env: RAILS=3.2.22 - - rvm: 1.9 - env: RAILS=master - allow_failures: - - env: RAILS=master -notifications: - irc: - channels: - - irc.freenode.org#activeadmin - on_success: change - on_failure: always - skip_join: true - template: - - "(%{branch}/%{commit} by %{author}): %{message} (%{build_url})" 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 20705ef90d1..00000000000 --- a/.yardopts +++ /dev/null @@ -1,8 +0,0 @@ -lib/**/*.rb ---protected ---no-private ---exclude (features|lib\/generators\/active_admin\/.*\/templates) -- -README.md -CHANGELOG.md -docs/**/*.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2681ffd0b75..dfb583197cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,80 +1,648 @@ # Changelog -## 1.0.0 [☰](https://github.com/activeadmin/activeadmin/compare/v0.6.3...master) (unreleased) + +[Future changelogs have moved to GitHub Releases](https://github.com/activeadmin/activeadmin/releases) + +## 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 + +* 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 + +* 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] + +## 3.0.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.14.0..v3.0.0) + +### Breaking Changes + +* Remove custom Ransack predicates that were MetaSearch backports. [#8010] by [@javierjulio] +* Require Ransack v4. [#8009] by [@javierjulio] + +### Enhancements + +* Use display name fallback if blank display name result. [#6342] by [@javierjulio] + +### Translation Improvements + +* Improve Swedish translations. [#7993] by [@carlottostromstedt] + +## 2.14.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.13.1..v2.14.0) + +### Enhancements + +* 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 + +* Add Ransack 3 compatibility. [#7453] by [@tagliala] + +### Bug Fixes + +* Fix pundit namespace detection. [#7144] by [@vlad-psh] + +### Documentation + +* Don't mention webpacker as the default asset generator in Rails. [#7377] by [@jaynetics] + +### Performance + +* Avoid duplicate work when downloading CSV. [#7336] by [@deivid-rodriguez] + +## 2.11.2 [☰](https://github.com/activeadmin/activeadmin/compare/v2.11.1..v2.11.2) + +### 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 + +* Add turbolinks support to has many js. [#7384] by [@amiel] + +### Documentation + +* Remove `insert_tag` from Form-Partial docs. [#7394] by [@TonyArra] + +## 2.11.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.10.1..v2.11.0) + +### Enhancements + +* Add Rails 7 Support. [#7235] by [@tagliala] + +### Bug Fixes + +* Fix form SCSS variables no longer being defined in the outermost scope, so no longer being accessible. [#7341] by [@gigorok] + +## 2.10.1 [☰](https://github.com/activeadmin/activeadmin/compare/v2.10.0..v2.10.1) + +### Enhancements + +* 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 + +* Fix frozen string error when downloading CSV and streaming disabled. [#7332] by [@deivid-rodriguez] + +## 2.10.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.9.0..v2.10.0) + +### Enhancements + +* 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 + +* Show ransackable_scopes filters in search results. [#7127] by [@vlad-psh] + +### Translation Improvements + +* 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] + +### Documentation + +* Replace deprecated update_attributes! with update!. [#6959] by [@sergey-alekseev] +* Clarify docs on user setup. [#6872] by [@javawizard] + +### 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 + +* 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 + +* Pick up upstream fixes in devise templates. [#6536] by [@munen] + +### Documentation + +* Fix `has_many` syntax in forms documentation. [#6583] by [@krzcho] +* Add example of using `default_main_content` in show pages. [#6487] by [@sjieg] + +### Dependency Changes + +* Remove sassc and sprockets runtime dependencies. [#6584] by [@deivid-rodriguez] + +## 2.8.1 [☰](https://github.com/activeadmin/activeadmin/compare/v2.8.0..v2.8.1) + +### Bug Fixes + +* 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] + +## 2.8.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.7.0..v2.8.0) + +### Enhancements + +* 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] + +### Bug Fixes + +* 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] + +## 2.5.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.4.0..v2.5.0) + +### Enhancements + +* Azerbaijani translation. [#5078] by [@orkhan] + +### Bug Fixes + +* 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 + +* 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] + +## 2.3.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.2.0..v2.3.0) + +### 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] + +### Bug Fixes + +* 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 + +* 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] + +### 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 + +* 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] + +### Dependency Changes + +* 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 + +* 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 + +* 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] + +### Dependency Changes + +* 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] + +## 1.4.3 [☰](https://github.com/activeadmin/activeadmin/compare/v1.4.2..v1.4.3) + +### Bug Fixes + +* 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] + +## 1.4.2 [☰](https://github.com/activeadmin/activeadmin/compare/v1.4.1..v1.4.2) + +### Bug Fixes + +* Fix `input_html` filter option evaluated only once. [#5376] by [@kjeldahl] + +## 1.4.1 [☰](https://github.com/activeadmin/activeadmin/compare/v1.4.0..v1.4.1) + +### 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 + +* 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 + +* 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 + +* 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 + +* 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) + +### 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 + +* 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] + +## 1.1.0 [☰](https://github.com/activeadmin/activeadmin/compare/v1.0.0..v1.1.0) + +### Bug Fixes + +* 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: + + ```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][] - -### Enhancements - -#### Major - -* Migration from Metasearch to Ransack [#1979][] by [@seanlinsley][] -* Rails 4 support [#2326][] by many people <3 -* Rails 4.2 support [#3731][] by [@gonzedge][] and [@timoschilling][] - -#### Minor - -* 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 +* 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] -* Accept block for download links [#2040][] by [@potatosalad][] -```ruby -index download_links: ->{ can?(:view_all_download_links) || [:pdf] } -``` + ```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 potential DOS attack via Ruby symbols [#1926][] by [@seanlinsley][] - * [this isn't an issue for those using Ruby >= 2.2](http://rubykaigi.org/2014/presentation/S-NarihiroNakamura) +* 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 -* 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][] +* 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` (`ActiveAdmin::EventDispatcher`). [#3435] by [@timoschilling] `ActiveAdmin::Event` will be removed in a future version, ActiveAdmin switched - to use `ActiveSupport::Notifications`. + 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" -``` + ```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](https://github.com/activeadmin/activeadmin/blob/0-6-stable/CHANGELOG.md) for 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 @@ -85,27 +653,381 @@ Please check [0-6-stable](https://github.com/activeadmin/activeadmin/blob/0-6-st [#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 -[@PChambino]: https://github.com/PChambino -[@TimPetricola]: https://github.com/TimPetricola +[#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 +[@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 +[@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 +[@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 aec68e849f3..f781d4bc14d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,144 +1,106 @@ -## Contributing +# Contributing -First off, thank you for considering contributing to Active Admin. It's people -like you that make Active Admin such a great tool. +Thanks for your interest in contributing to ActiveAdmin! Please take a moment to review this document **before submitting a pull request**. -### 1. Where do I go from here? +## Pull requests -If you've noticed a bug or have a question that doesn't belong on the -[mailing list](http://groups.google.com/group/activeadmin) or -[Stack Overflow](http://stackoverflow.com/questions/tagged/activeadmin), -[search the issue tracker](https://github.com/activeadmin/activeadmin/issues?q=something) -to see if someone else in the community has already created a ticket. -If not, go ahead and [make one](https://github.com/activeadmin/activeadmin/issues/new)! +**Please ask first before starting work on any significant new features.** -### 2. Fork & create a branch +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! -If this is something you think you can fix, then -[fork Active Admin](https://help.github.com/articles/fork-a-repo) -and create a branch with a descriptive name. +Please ensure that the tests are passing when submitting a pull request. If you're adding new features to ActiveAdmin, please include tests. -A good branch name would be (where issue #325 is the ticket you're working on): +## Where do I go from here? -```sh -git checkout -b 325-add-japanese-translations -``` +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 -### 3. Get the test suite running +If this is something you think you can fix, then [fork Active Admin] and create +a branch with a descriptive name. -Install the development dependencies: +### Get the test suite running + +Make sure you're using a recent Ruby and Node version. You'll also need Chrome installed in order to run Cucumber scenarios. + +Now install the development dependencies: ```sh +gem install foreman bundle install +yarn install ``` Now you should be able to run the entire suite using: ```sh -rake test +bin/rake ``` -Which will generate a rails application in `spec/rails` to run the tests against. +The task will generate a sample Rails application in `tmp/test_apps` to run the +test suite against. -If your tests are passing locally but they're failing on Travis, reset your test environment: +If you want to test against a Rails version different from the latest, make sure +you use the correct Gemfile, for example: ```sh -rm -rf spec/rails && bundle update +export BUNDLE_GEMFILE=gemfiles/rails_61/Gemfile ``` -### 4. Implement your fix or feature - -At this point, you're ready to make your changes! Feel free to ask for help; -everyone is a beginner at first :smile_cat: +### Implement your fix or feature -### 5. View your changes in a Rails application +At this point, you're ready to make your changes. Feel free to ask for help. -Active Admin is meant to be used by humans, not cucumbers. So make sure to take -a look at your changes in a browser. +### View your changes in a Rails application -To boot up a test Rails app: +Make sure to take a look at your changes in a browser. To boot up a test Rails app: ```sh -script/local server +bin/rake local server ``` -This will automatically create a Rails app if none already exists, and store it in the -`.test-rails-apps` folder. The currently active app is symlinked to `test-rails-app`. - -If you have any Bundler issues, call the `use_rails` script then prepend -the version of rails you would like to use in an environment variable: - -```sh -script/use_rails 4.0.0 -RAILS=4.0.0 script/local server -``` - -You should now be able to open in your browser. You can log in using: - - User: admin@example.com - Password: password +This will automatically create a Rails app if none already exists, and store it +in the `tmp/development_apps` folder. -If you need to perform any other commands on the test application, use the -`local` script. For example: +You should now be able to open in your browser and log in using `admin@example.com` and `password`. -To boot the rails console: +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: ```sh -script/local console +bin/rake local console ``` -Or to migrate the database: +Or to migrate the database for a new migration: ```sh -script/local rake db:migrate +bin/rake local db:migrate ``` -### 6. Run tests against major supported rails versions +### Create a Pull Request -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. +At this point, if your changes look good and tests are passing, you are ready to create a pull request. -```sh -rake test:major_supported_rails -``` +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. -This runs our test suite against a couple of major versions of Rails. -Travis does essentially the same thing when you open a Pull Request. -We care about quality, so your PR won't be merged until all tests pass. +## Commit messages -### 7. Make a Pull Request +Try your best to follow these [seven rules for a great commit message](https://cbea.ms/git-commit/#seven-rules). -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: +## Merging a PR (maintainers only) -```sh -git remote add upstream git@github.com:activeadmin/activeadmin.git -git checkout master -git pull upstream master -``` +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. -Then update your feature branch from your local copy of master, and push it! +## Shipping a release (maintainers only) -```sh -git checkout 325-add-japanese-translations -git rebase master -git push --set-upstream origin 325-add-japanese-translations -``` +Maintainers need to do the following to push out a release: -Finally, go to GitHub and [make a Pull Request](https://help.github.com/articles/creating-a-pull-request) :D +* 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. -### 8. Keeping your Pull Request updated - -If a maintainer asks you to "rebase" your PR, they're saying that a lot of code -has changed, and that you need to update your branch so it's easier to merge. - -To learn more about rebasing in Git, there are a lot of -[good](http://git-scm.com/book/en/Git-Branching-Rebasing) -[resources](https://help.github.com/articles/interactive-rebase), -but here's the suggested workflow: - -```sh -git checkout 325-add-japanese-translations -git pull --rebase upstream master -git push -f 325-add-japanese-translations -``` +[new issue]: https://github.com/activeadmin/activeadmin/issues/new diff --git a/Gemfile b/Gemfile index b6b56851419..de11eb6d653 100644 --- a/Gemfile +++ b/Gemfile @@ -1,66 +1,53 @@ -source 'https://rubygems.org' +# frozen_string_literal: true +source "https://rubygems.org" -gemspec +group :development, :test do + gem "rake" -require File.expand_path 'spec/support/detect_rails_version', File.dirname(__FILE__) + gem "cancancan" + gem "pundit" -rails_version = detect_rails_version -gem 'rails', rails_version == 'master' ? {github: 'rails/rails'} : rails_version + gem "draper" + gem "devise", "~> 4.9" # TODO: relax this dependency when formtastic/formtastic#1401 will be fixed -gem 'jquery-ui-rails', rails_version[0] == '3' ? '~> 4.0' : '~> 5.0' + gem "rails", "~> 8.1.0" -if rails_version == 'master' - gem 'arel', github: 'rails/arel' - gem 'sprockets', github: 'rails/sprockets' - gem 'sass-rails', github: 'rails/sass-rails' - gem 'rack', github: 'rack/rack' - gem 'sprockets-rails', '3.0.0.beta2' -end - -# Optional dependencies -gem 'cancan' -gem 'devise' -gem 'draper' -gem 'pundit' - -# Utility gems used in both development & test environments -gem 'rake', require: false -gem 'parallel_tests' - -# Debugging -gem 'pry' # Easily debug from your console with `binding.pry` - -group :development do - # Debugging - gem 'better_errors' # Web UI to debug exceptions. Go to /__better_errors to access the latest one - gem 'binding_of_caller', platforms: :mri # Retrieve the binding of a method's caller in MRI Ruby >= 1.9.2 + gem "sprockets-rails" + gem "ransack", ">= 4.2.0" + gem "formtastic", ">= 5.0.0" - # Performance - gem 'rack-mini-profiler' # Inline app profiler. See ?pp=help for options. - gem 'flamegraph', platforms: :mri # Flamegraph visualiztion: ?pp=flamegraph - - # Documentation - gem 'yard' # Documentation generator - gem 'redcarpet', platforms: :mri # Markdown implementation (for yard) - gem 'kramdown', platforms: :jruby # Markdown implementation (for yard) + gem "cssbundling-rails" + gem "importmap-rails" end group :test do - gem 'capybara' - gem 'simplecov', require: false # Test coverage generator. Go to /coverage/ after running tests - gem 'coveralls', require: false # Test coverage website. Go to https://coveralls.io - gem 'cucumber-rails', require: false - gem 'database_cleaner' - gem 'guard-rspec' - gem 'jasmine' - gem 'jslint_on_rails' - gem 'launchy' - gem 'rails-i18n' # Provides default i18n for many languages - gem 'rspec' - gem 'rspec-rails', '~> 3.1.0' - gem 'i18n-spec' - gem 'shoulda-matchers', '<= 2.8.0' - gem 'sqlite3', platforms: :mri - gem 'activerecord-jdbcsqlite3-adapter', platforms: :jruby - gem 'poltergeist' + 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 "cucumber" + 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 :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..858dc3b7ff3 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,504 @@ +PATH + remote: . + specs: + activeadmin (4.0.0.beta21) + arbre (~> 2.0) + csv + formtastic (>= 5.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.16) + railties + actioncable (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) + mail (>= 2.8.0) + actionmailer (8.1.2) + actionpack (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activesupport (= 8.1.2) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.2) + actionview (= 8.1.2) + activesupport (= 8.1.2) + 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.2) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.2) + activesupport (= 8.1.2) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.2) + activesupport (= 8.1.2) + globalid (>= 0.3.6) + activemodel (8.1.2) + activesupport (= 8.1.2) + activemodel-serializers-xml (1.0.3) + activemodel (>= 5.0.0.a) + activesupport (>= 5.0.0.a) + builder (~> 3.1) + activerecord (8.1.2) + activemodel (= 8.1.2) + activesupport (= 8.1.2) + timeout (>= 0.4.0) + activestorage (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activesupport (= 8.1.2) + marcel (~> 1.0) + activesupport (8.1.2) + 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.8.8) + 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.21) + bigdecimal (4.0.1) + 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.6) + connection_pool (3.0.2) + crass (1.0.6) + cssbundling-rails (1.4.3) + railties (>= 6.0.0) + csv (3.3.5) + cucumber (10.2.0) + base64 (~> 0.2) + builder (~> 3.2) + cucumber-ci-environment (> 9, < 12) + cucumber-core (> 15, < 17) + cucumber-cucumber-expressions (> 17, < 20) + cucumber-html-formatter (> 21, < 23) + diff-lcs (~> 1.5) + logger (~> 1.6) + mini_mime (~> 1.1) + multi_test (~> 1.1) + sys-uname (~> 1.3) + cucumber-ci-environment (11.0.0) + cucumber-core (16.1.1) + cucumber-gherkin (> 36, < 40) + cucumber-messages (> 31, < 33) + cucumber-tag-expressions (> 6, < 9) + cucumber-cucumber-expressions (18.1.0) + bigdecimal + cucumber-gherkin (38.0.0) + cucumber-messages (>= 31, < 33) + cucumber-html-formatter (22.3.0) + cucumber-messages (> 23, < 33) + cucumber-messages (32.0.1) + cucumber-rails (4.0.0) + capybara (>= 3.25, < 4) + cucumber (>= 7, < 11) + railties (>= 6.1, < 9) + cucumber-tag-expressions (8.1.0) + cuprite (0.17) + capybara (~> 3.0) + ferrum (~> 0.17.0) + database_cleaner-active_record (2.2.2) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0) + database_cleaner-core (2.0.1) + date (3.5.1) + devise (4.9.4) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) + diff-lcs (1.6.2) + docile (1.4.1) + 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.1) + erubi (1.13.1) + ferrum (0.17.1) + addressable (~> 2.5) + base64 (~> 0.2) + concurrent-ruby (~> 1.1) + webrick (~> 1.7) + websocket-driver (~> 0.7) + ffi (1.17.3-aarch64-linux-gnu) + ffi (1.17.3-arm64-darwin) + ffi (1.17.3-x86_64-darwin) + ffi (1.17.3-x86_64-linux-gnu) + formtastic (5.0.0) + actionpack (>= 6.0.0) + formtastic_i18n (0.7.0) + globalid (1.3.0) + activesupport (>= 6.1) + has_scope (0.9.0) + actionpack (>= 7.0) + activesupport (>= 7.0) + highline (3.1.2) + reline + i18n (1.14.8) + 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.8.2) + irb (1.16.0) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + iso (0.4.0) + i18n + json (2.18.0) + 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.5) + 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.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + matrix (0.4.3) + memoist3 (1.0.0) + mini_mime (1.1.5) + minitest (6.0.1) + prism (~> 1.5) + multi_test (1.1.0) + net-imap (0.6.2) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.0-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.0-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.0-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.0-x86_64-linux-gnu) + racc (~> 1.4) + orm_adapter (0.5.0) + parallel (1.27.0) + parallel_tests (5.5.0) + parallel + parser (3.3.10.1) + ast (~> 2.4.1) + racc + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.8.0) + psych (5.3.1) + date + stringio + public_suffix (7.0.2) + pundit (2.5.2) + activesupport (>= 3.0.0) + racc (1.8.1) + rack (3.2.4) + rack-session (2.1.1) + 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.2) + actioncable (= 8.1.2) + actionmailbox (= 8.1.2) + actionmailer (= 8.1.2) + actionpack (= 8.1.2) + actiontext (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activemodel (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) + bundler (>= 1.15.0) + railties (= 8.1.2) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + 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.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) + 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.3.1) + ransack (4.4.1) + activerecord (>= 7.2) + activesupport (>= 7.2) + i18n + rdoc (7.1.0) + erb + psych (>= 4.0.0) + tsort + regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) + request_store (1.7.0) + rack (>= 1.4) + responders (3.2.0) + 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.7) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.2) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.6) + rubocop (1.82.1) + 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.48.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.0) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-capybara (2.22.1) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-packaging (0.6.0) + lint_roller (~> 1.1.0) + rubocop (>= 1.72.1, < 2.0) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.34.3) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rspec (3.9.0) + lint_roller (~> 1.1) + rubocop (~> 1.81) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + securerandom (0.4.1) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-cobertura (3.1.0) + rexml + simplecov (~> 0.19) + simplecov-html (0.13.2) + simplecov_json_formatter (0.1.4) + sprockets (4.2.2) + concurrent-ruby (~> 1.0) + logger + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + sprockets (>= 3.0.0) + sqlite3 (2.9.0-aarch64-linux-gnu) + sqlite3 (2.9.0-arm64-darwin) + sqlite3 (2.9.0-x86_64-darwin) + sqlite3 (2.9.0-x86_64-linux-gnu) + stringio (3.2.0) + sys-uname (1.4.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.0) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.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.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.7.4) + +PLATFORMS + aarch64-linux + arm64-darwin + x86_64-darwin + x86_64-linux + +DEPENDENCIES + activeadmin! + cancancan + capybara + cssbundling-rails + cucumber + cucumber-rails + cuprite + database_cleaner-active_record + devise (~> 4.9) + draper + formtastic (>= 5.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.4 diff --git a/Guardfile b/Guardfile deleted file mode 100644 index ada15e73388..00000000000 --- a/Guardfile +++ /dev/null @@ -1,8 +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/" } - watch('spec/rails_helper.rb') { "spec/" } -end diff --git a/README.md b/README.md index 78d820bac44..90b76bb70b3 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,104 @@ # Active Admin -Active Admin is a Ruby on Rails framework for creating elegant backends for website administration. +[Active Admin](https://activeadmin.info) is a Ruby on Rails framework for +creating elegant backends for website administration. -[![Version ](http://img.shields.io/gem/v/activeadmin.svg) ](https://rubygems.org/gems/activeadmin) -[![Travis CI ](http://img.shields.io/travis/activeadmin/activeadmin/master.svg) ](https://travis-ci.org/activeadmin/activeadmin) -[![Quality ](http://img.shields.io/codeclimate/github/activeadmin/activeadmin.svg) ](https://codeclimate.com/github/activeadmin/activeadmin) -[![Coverage ](http://img.shields.io/coveralls/activeadmin/activeadmin.svg) ](https://coveralls.io/r/activeadmin/activeadmin) -[![Inch CI ](http://inch-ci.org/github/activeadmin/activeadmin.svg?branch=master) ](http://inch-ci.org/github/activeadmin/activeadmin) +[![Version][rubygems_badge]][rubygems] +[![Github Actions][actions_badge]][actions] +[![Coverage][coverage_badge]][coverage] +[![Tidelift][tidelift_badge]][tidelift] -## State of the project - -### 1.0.0 - -We're [currently working on 1.0.0](https://github.com/activeadmin/activeadmin/issues?milestone=18), -which as far as dependencies, moves us from meta_search to Ransack and adds Rails 4 support. +## Goals -You can get it by tracking master: -```ruby -gem 'activeadmin', github: 'activeadmin' -``` +* 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. -Or you can using rubygems: -```ruby -gem 'activeadmin', '~> 1.0.0.pre2' -``` +## Getting started -*Keep in mind that during the time where we use `pre`-release label, things can break in each release!* +* 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. -### 0.6.x +## For enterprise -The plan is to follow [semantic versioning](http://semver.org/) as of 1.0.0. The 0.6.x line will -still be maintained, and we will backport bug fixes into future 0.6.x releases. If you don't want -to have to wait for a release, you can track the branch instead: +Active Admin for enterprise is available via the Tidelift subscription. [Learn +More][tidelift_enterprise]. -```ruby -gem 'activeadmin', github: 'activeadmin', branch: '0-6-stable' -``` +## Want to contribute? -## Documentation +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. -Please note that is out of date. For the latest docs, check out the -Github [docs](https://github.com/activeadmin/activeadmin/tree/master/docs#activeadmin-documentation) and the [wiki](https://github.com/activeadmin/activeadmin/wiki). +## Want to support us? -## Links +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. -* Website: (out of date) -* Live demo: -* Documentation - * Guides: - * YARD: - * Wiki: +You can also support us with a weekly tip via [Liberapay]. -## Goals +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. -1. Enable developers to quickly create good-looking administration interfaces. -2. Build a DSL for developers and an interface for businesses. -3. Ensure that developers can easily customize every nook and cranny. +## Dependencies -## Getting started +We try not to reinvent the wheel, so Active Admin is built with other open source projects: -Check out [the docs](https://github.com/activeadmin/activeadmin/blob/master/docs/0-installation.md)! +* [Arbre] +* [Devise] +* [Flowbite](https://flowbite.com) +* [Formtastic] +* [Inherited Resources] +* [Kaminari] +* [Ransack] +* [Tailwind CSS](https://tailwindcss.com) -## Need help? +## Security contact information -Ask us in IRC ([#activeadmin](https://webchat.freenode.net/?channels=activeadmin)), on the -[mailing list](http://groups.google.com/group/activeadmin), or on -[Stack Overflow](http://stackoverflow.com/questions/tagged/activeadmin). +Please use the Tidelift security contact to [report a security vulnerability][Tidelift security contact]. +Tidelift will coordinate the fix and disclosure. -## Want to contribute? +## Acknowledgements -The [contributing guide](https://github.com/activeadmin/activeadmin/blob/master/CONTRIBUTING.md) -is a good place to start. If you have questions, feel free to ask -[@seanlinsley](https://twitter.com/seanlinsley) or [@captainhagbard](https://twitter.com/captainhagbard). +Thanks to [Greg Bell][Greg] for creating and sharing this project with the open source community. -## Dependencies +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. -We try not to reinvent the wheel, so Active Admin is built with other open source projects: +Thanks to [Tidelift][tidelift] and all our Tidelift subscribers. -Tool | Description ---------------------- | ----------- -[Arbre] | Ruby -> HTML, just like that. -[Devise] | Powerful, extensible user authentication -[Formtastic] | A Rails form builder plugin with semantically rich and accessible markup -[Inherited Resources] | Simplifies controllers with pre-built RESTful controller actions -[Kaminari] | Elegant pagination for any sort of collection -[Ransack] | Provides a simple search API to query your data +Thanks to [Open Collective][opencollective contributors] and all our Open Collective contributors. [Arbre]: https://github.com/activeadmin/arbre -[Devise]: https://github.com/plataformatec/devise -[Formtastic]: https://github.com/justinfrench/formtastic -[Inherited Resources]: https://github.com/josevalim/inherited_resources -[Kaminari]: https://github.com/amatsuda/kaminari +[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/Rakefile b/Rakefile index dcb8aa60b9f..e26229820b9 100644 --- a/Rakefile +++ b/Rakefile @@ -1,33 +1,21 @@ -require 'bundler' -require 'rake' -Bundler.setup -Bundler::GemHelper.install_tasks +# frozen_string_literal: true +require "bundler/gem_tasks" -def cmd(command) - puts command - fail unless system command -end +import "tasks/local.rake" +import "tasks/test.rake" +import "tasks/dependencies.rake" -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 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 -end - task :console do - require 'irb' - require 'irb/completion' - + require "irb" + require "irb/completion" ARGV.clear IRB.start end diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 00000000000..6159e2d08aa --- /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.beta21"` 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. + +``` +yarn add @activeadmin/activeadmin@4.0.0-beta21 +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 a4266b10c1a..e7ce58b7832 100644 --- a/activeadmin.gemspec +++ b/activeadmin.gemspec @@ -1,30 +1,42 @@ -require File.join(File.dirname(__FILE__), "lib", "active_admin", "version") +# frozen_string_literal: true +require File.join(__dir__, "lib", "active_admin", "version") Gem::Specification.new do |s| - s.name = 'activeadmin' - s.license = 'MIT' - s.version = ActiveAdmin::VERSION - s.homepage = 'http://activeadmin.info' - s.authors = ['Greg Bell'] - s.email = ['gregdbell@gmail.com'] - s.description = 'The administration framework for Ruby on Rails.' - s.summary = '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 -- {spec,features}/*`.split("\n") + s.files = Dir["LICENSE", "plugin.js", 'config/importmap.rb', "{app,config/locales,lib,vendor}/**/{.*,*}"].reject { |f| File.directory?(f) } - s.required_ruby_version = '>= 1.9.3' + s.extra_rdoc_files = %w[CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md README.md UPGRADING.md] - s.add_dependency 'arbre', '~> 1.0', '>= 1.0.2' - s.add_dependency 'bourbon' - s.add_dependency 'coffee-rails' - s.add_dependency 'formtastic', '~> 3.1' - s.add_dependency 'formtastic_i18n' - s.add_dependency 'inherited_resources', '~> 1.6' - s.add_dependency 'jquery-rails' - s.add_dependency 'jquery-ui-rails' - s.add_dependency 'kaminari', '~> 0.15' - s.add_dependency 'rails', '>= 3.2', '< 5.0' - s.add_dependency 'ransack', '~> 1.3' - s.add_dependency 'sass-rails' + 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.2" + + s.add_dependency "arbre", "~> 2.0" + s.add_dependency "csv" + s.add_dependency "formtastic", ">= 5.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/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/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.coffee b/app/assets/javascripts/active_admin/application.js.coffee deleted file mode 100644 index 8d572cecea2..00000000000 --- a/app/assets/javascripts/active_admin/application.js.coffee +++ /dev/null @@ -1,37 +0,0 @@ -# Initializers -$(document).on 'ready page:load', -> - # jQuery datepickers (also evaluates dynamically added HTML) - $(document).on 'focus', 'input.datepicker:not(.hasDatepicker)', -> - $input = $(@) - - # Only applying datepicker to compatible browsers - return if $input[0].type is 'date' - - defaults = dateFormat: 'yy-mm-dd' - options = $input.data 'datepicker-options' - $input.datepicker $.extend(defaults, options) - - # Clear Filters button - $('.clear_filters_btn').click -> - params = window.location.search.slice(1).split('&') - regex = /^(q\[|q%5B|q%5b|page|commit)/ - window.location.search = (param for param in params when not param.match(regex)).join('&') - - # 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}]" - - # Tab navigation - $('#active_admin_content .tabs').tabs() - - # In order for index scopes to overflow properly onto the next line, we have - # to manually set its width based on the width of the batch action button. - if (batch_actions_selector = $('.table_tools .batch_actions_selector')).length - batch_actions_selector.next().css - width: "calc(100% - 10px - #{batch_actions_selector.outerWidth()}px)" - 'float': 'right' diff --git a/app/assets/javascripts/active_admin/base.js.coffee b/app/assets/javascripts/active_admin/base.js.coffee deleted file mode 100644 index bf421a2a8d2..00000000000 --- a/app/assets/javascripts/active_admin/base.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -#= require jquery -#= require ./jquery_ui -#= require jquery_ujs -#= require_self -#= require_tree ./lib -#= require_tree ./ext -#= require ./application - -window.ActiveAdmin = {} diff --git a/app/assets/javascripts/active_admin/ext/jquery-ui.js.coffee b/app/assets/javascripts/active_admin/ext/jquery-ui.js.coffee deleted file mode 100644 index 68cd7c63496..00000000000 --- a/app/assets/javascripts/active_admin/ext/jquery-ui.js.coffee +++ /dev/null @@ -1,6 +0,0 @@ -# Short-circuits `_focusTabbable` to focus on the modal itself instead of -# elements inside the modal. Without this, if a datepicker is the first input, -# it'll immediately pop up when the modal opens. -# See this ticket for more info: http://bugs.jqueryui.com/ticket/4731 -$.ui.dialog.prototype._focusTabbable = -> - @uiDialog.focus() diff --git a/app/assets/javascripts/active_admin/ext/jquery.js.coffee b/app/assets/javascripts/active_admin/ext/jquery.js.coffee deleted file mode 100644 index c79b22ceb92..00000000000 --- a/app/assets/javascripts/active_admin/ext/jquery.js.coffee +++ /dev/null @@ -1,7 +0,0 @@ -# `serializeArray` generates => [{ name: 'foo', value: 'bar' }] -# This function remaps it to => { foo: 'bar' } -$.fn.serializeObject = -> - obj = {} - for o in @serializeArray() - obj[o.name] = o.value - obj diff --git a/app/assets/javascripts/active_admin/jquery_ui.js.erb b/app/assets/javascripts/active_admin/jquery_ui.js.erb deleted file mode 100644 index c40b276b7f2..00000000000 --- a/app/assets/javascripts/active_admin/jquery_ui.js.erb +++ /dev/null @@ -1,11 +0,0 @@ -<% jquery_ui_path = if Jquery::Ui::Rails::VERSION >= "5" - "jquery-ui/" - else - "jquery.ui." - end -%> -<% require_asset "#{jquery_ui_path}datepicker" %> -<% require_asset "#{jquery_ui_path}dialog" %> -<% require_asset "#{jquery_ui_path}sortable" %> -<% require_asset "#{jquery_ui_path}widget" %> -<% require_asset "#{jquery_ui_path}tabs" %> diff --git a/app/assets/javascripts/active_admin/lib/batch_actions.js.coffee b/app/assets/javascripts/active_admin/lib/batch_actions.js.coffee deleted file mode 100644 index 42013a07874..00000000000 --- a/app/assets/javascripts/active_admin/lib/batch_actions.js.coffee +++ /dev/null @@ -1,39 +0,0 @@ -$(document).on 'ready page:load', -> - - # - # Use ActiveAdmin.modal_dialog to prompt user if confirmation is required for current Batch Action - # - $('.batch_actions_selector li a').click (e)-> - e.stopPropagation() # prevent Rails UJS click event - e.preventDefault() - if message = $(@).data 'confirm' - ActiveAdmin.modal_dialog message, $(@).data('inputs'), (inputs)=> - $(@).trigger 'confirm:complete', inputs - else - $(@).trigger 'confirm:complete' - - $('.batch_actions_selector li a').on 'confirm:complete', (e, inputs)-> - if val = JSON.stringify inputs - $('#batch_action_inputs').val val - else - $('#batch_action_inputs').attr 'disabled', 'disabled' - - $('#batch_action').val $(@).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() - - $(document).on 'change', '.paginated_collection :checkbox', -> - if $(".paginated_collection :checkbox:checked").length - $(".batch_actions_selector").each -> $(@).aaDropdownMenu("enable") - else - $(".batch_actions_selector").each -> $(@).aaDropdownMenu("disable") diff --git a/app/assets/javascripts/active_admin/lib/checkbox-toggler.js.coffee b/app/assets/javascripts/active_admin/lib/checkbox-toggler.js.coffee deleted file mode 100644 index 57be96e4f78..00000000000 --- a/app/assets/javascripts/active_admin/lib/checkbox-toggler.js.coffee +++ /dev/null @@ -1,36 +0,0 @@ -class ActiveAdmin.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) - -$.widget.bridge 'checkboxToggler', ActiveAdmin.CheckboxToggler diff --git a/app/assets/javascripts/active_admin/lib/dropdown-menu.js.coffee b/app/assets/javascripts/active_admin/lib/dropdown-menu.js.coffee deleted file mode 100644 index 446beb2debd..00000000000 --- a/app/assets/javascripts/active_admin/lib/dropdown-menu.js.coffee +++ /dev/null @@ -1,101 +0,0 @@ -class ActiveAdmin.DropdownMenu - - constructor: (@options, @element) -> - @$element = $(@element) - - defaults = { - fadeInDuration: 20, - fadeOutDuration: 100, - onClickActionItemCallback: null - } - - @options = $.extend defaults, @options - @isOpen = false - - @$menuButton = @$element.find '.dropdown_menu_button' - @$menuList = @$element.find '.dropdown_menu_list_wrapper' - - @_buildMenuList() - @_bind() - - open: -> - @isOpen = true - @$menuList.fadeIn @options.fadeInDuration - - @_position() - @ - - - close: -> - @isOpen = false - @$menuList.fadeOut this.options.fadeOutDuration - @ - - destroy: -> - @$element.unbind() - @$element = null - @ - - isDisabled: -> - @$menuButton.hasClass 'disabled' - - disable: -> - @$menuButton.addClass 'disabled' - - enable: -> - @$menuButton.removeClass 'disabled' - - option: (key, value) -> - if $.isPlainObject(key) - @options = $.extend(true, @options, key) - else if key? - @options[key] - else - @options[key] = value - - # Private - - _buildMenuList: -> - @$nipple = $('') - @$menuList.prepend @$nipple - @$menuList.hide() - - _bind: -> - $('body').click => - @close() if @isOpen - - @$menuButton.click => - unless @isDisabled() - if @isOpen then @close() else @open() - false - - _position: -> - @$menuList.css 'top', @$menuButton.position().top + @$menuButton.outerHeight() + 10 - - button_left = @$menuButton.position().left - button_center = @$menuButton.outerWidth() / 2 - button_right = button_left + button_center * 2 - menu_center = @$menuList.outerWidth() / 2 - nipple_center = @$nipple.outerWidth() / 2 - window_right = $(window).width() - - centered_menu_left = button_left + button_center - menu_center - centered_menu_right = button_left + button_center + menu_center - - if centered_menu_left < 0 - # Left align with button - @$menuList.css 'left', button_left - @$nipple.css 'left', button_center - nipple_center - else if centered_menu_right > window_right - # Right align with button - @$menuList.css 'right', window_right - button_right - @$nipple.css 'right', button_center - nipple_center - else - # Center align under button - @$menuList.css 'left', centered_menu_left - @$nipple.css 'left', menu_center - nipple_center - -$.widget.bridge 'aaDropdownMenu', ActiveAdmin.DropdownMenu - -$(document).on 'ready page:load', -> - $('.dropdown_menu').aaDropdownMenu() diff --git a/app/assets/javascripts/active_admin/lib/flash.js.coffee b/app/assets/javascripts/active_admin/lib/flash.js.coffee deleted file mode 100644 index 734abb0b32e..00000000000 --- a/app/assets/javascripts/active_admin/lib/flash.js.coffee +++ /dev/null @@ -1,19 +0,0 @@ -ActiveAdmin.flash = - class Flash - @error: (message, close_after) -> - new @ message, "error", close_after - @notice: (message, close_after) -> - new @ message, "notice", close_after - reference: -> - @reference - constructor: (@message, @type = "notice", close_after) -> - @reference = jQuery("
").addClass("flash flash_#{@type}").text(@message) - jQuery ".flashes" - .append @reference - @close_after close_after if close_after? - close_after: (close_after) -> - setTimeout => - @close() - , close_after * 1000 - close: -> - @reference.remove() diff --git a/app/assets/javascripts/active_admin/lib/has_many.js.coffee b/app/assets/javascripts/active_admin/lib/has_many.js.coffee deleted file mode 100644 index bfd5fc82c39..00000000000 --- a/app/assets/javascripts/active_admin/lib/has_many.js.coffee +++ /dev/null @@ -1,79 +0,0 @@ -$ -> - # Provides a before-removal hook: - # $ -> - # # This is a good place to tear down JS plugins to prevent memory leaks. - # $(document).on 'has_many_remove:before', '.has_many_container', (e, fieldset, container)-> - # fieldset.find('.select2').select2 'destroy' - # - # # If you need to do anything after removing the items you can use the - # has_many_remove:after hook - # $(document).on 'has_many_remove:after', '.has_many_container', (e, fieldset, container)-> - # list_item_count = container.find('.has_many_fields').length - # alert("There are now #{list_item_count} items in the list") - # - $(document).on 'click', 'a.button.has_many_remove', (e)-> - e.preventDefault() - parent = $(@).closest '.has_many_container' - to_remove = $(@).closest 'fieldset' - recompute_positions parent - - parent.trigger 'has_many_remove:before', [to_remove, parent] - to_remove.remove() - parent.trigger 'has_many_remove:after', [to_remove, parent] - - # Provides before and after creation hooks: - # $ -> - # # The before hook allows you to prevent the creation of new records. - # $(document).on 'has_many_add:before', '.has_many_container', (e, container)-> - # if $(@).children('fieldset').length >= 3 - # alert "you've reached the maximum number of items" - # e.preventDefault() - # - # # The after hook is a good place to initialize JS plugins and the like. - # $(document).on 'has_many_add:after', '.has_many_container', (e, fieldset, container)-> - # fieldset.find('select').chosen() - # - $(document).on 'click', 'a.button.has_many_add', (e)-> - e.preventDefault() - parent = $(@).closest '.has_many_container' - parent.trigger before_add = $.Event('has_many_add:before'), [parent] - - unless before_add.isDefaultPrevented() - index = parent.data('has_many_index') || parent.children('fieldset').length - 1 - parent.data has_many_index: ++index - - regex = new RegExp $(@).data('placeholder'), 'g' - html = $(@).data('html').replace regex, index - - fieldset = $(html).insertBefore(@) - recompute_positions parent - parent.trigger 'has_many_add:after', [fieldset, parent] - - $(document).on 'change','.has_many_container[data-sortable] :input[name$="[_destroy]"]', -> - recompute_positions $(@).closest '.has_many' - - init_sortable() - $(document).on 'has_many_add:after', '.has_many_container', init_sortable - - -init_sortable = -> - elems = $('.has_many_container[data-sortable]:not(.ui-sortable)') - elems.sortable \ - items: '> fieldset', - handle: '> ol > .handle', - stop: recompute_positions - elems.each recompute_positions - -recompute_positions = (parent)-> - parent = if parent instanceof jQuery then parent else $(@) - input_name = parent.data 'sortable' - position = parseInt(parent.data('sortable-start') || 0, 10) - - parent.children('fieldset').each -> - # We ignore nested inputs, so when defining your has_many, be sure to keep - # your sortable input at the root of the has_many block. - destroy_input = $(@).find "> ol > .input > :input[name$='[_destroy]']" - sortable_input = $(@).find "> ol > .input > :input[name$='[#{input_name}]']" - - if sortable_input.length - sortable_input.val if destroy_input.is ':checked' then '' else position++ diff --git a/app/assets/javascripts/active_admin/lib/modal_dialog.js.coffee b/app/assets/javascripts/active_admin/lib/modal_dialog.js.coffee deleted file mode 100644 index 9ea767b639b..00000000000 --- a/app/assets/javascripts/active_admin/lib/modal_dialog.js.coffee +++ /dev/null @@ -1,45 +0,0 @@ -ActiveAdmin.modal_dialog = (message, inputs, callback)-> - html = """
    """ - for name, type of inputs - if /^(datepicker|checkbox|text)$/.test type - wrapper = 'input' - else if type is 'textarea' - wrapper = 'textarea' - else if $.isArray type - [wrapper, elem, opts, type] = ['select', 'option', type, ''] - else - throw new Error "Unsupported input type: {#{name}: #{type}}" - - klass = if type is 'datepicker' then type else '' - html += """
  • - - <#{wrapper} name="#{name}" class="#{klass}" type="#{type}">""" + - (if opts then ( - for v in opts - $elem = $("<#{elem}/>") - if $.isArray v - $elem.text(v[0]).val(v[1]) - else - $elem.text(v) - $elem.wrap('
    ').parent().html() - ).join '' else '') + - "" + - "
  • " - [wrapper, elem, opts, type, klass] = [] # unset any temporary variables - - html += "
" - - form = $(html).appendTo('body') - $('body').trigger 'modal_dialog:before_open', [form] - - form.dialog - modal: true - open: (event, ui) -> - $('body').trigger 'modal_dialog:after_open', [form] - dialogClass: 'active_admin_dialog' - buttons: - OK: -> - callback $(@).serializeObject() - $(@).dialog('close') - Cancel: -> - $(@).dialog('close').remove() diff --git a/app/assets/javascripts/active_admin/lib/per_page.js.coffee b/app/assets/javascripts/active_admin/lib/per_page.js.coffee deleted file mode 100644 index 08ea65c836d..00000000000 --- a/app/assets/javascripts/active_admin/lib/per_page.js.coffee +++ /dev/null @@ -1,31 +0,0 @@ -class ActiveAdmin.PerPage - constructor: (@options, @element)-> - @$element = $(@element) - @_init() - @_bind() - - _init: -> - @$params = @_queryParams() - - _bind: -> - @$element.change => - @$params['per_page'] = @$element.val() - delete @$params['page'] - location.search = $.param(@$params) - - _queryParams: -> - query = window.location.search.substring(1) - params = {} - re = /([^&=]+)=([^&]*)/g - while m = re.exec(query) - params[@_decode(m[1])] = @_decode(m[2]) - params - - _decode: (value) -> - #replace "+" before decodeURIComponent - decodeURIComponent(value.replace(/\+/g, '%20')) - -$.widget.bridge 'perPage', ActiveAdmin.PerPage - -$ -> - $('.pagination_per_page select').perPage() diff --git a/app/assets/javascripts/active_admin/lib/table-checkbox-toggler.js.coffee b/app/assets/javascripts/active_admin/lib/table-checkbox-toggler.js.coffee deleted file mode 100644 index 25ec31c5877..00000000000 --- a/app/assets/javascripts/active_admin/lib/table-checkbox-toggler.js.coffee +++ /dev/null @@ -1,24 +0,0 @@ -class ActiveAdmin.TableCheckboxToggler extends ActiveAdmin.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() - -$.widget.bridge 'tableCheckboxToggler', ActiveAdmin.TableCheckboxToggler diff --git a/app/assets/stylesheets/active_admin/_base.scss b/app/assets/stylesheets/active_admin/_base.scss deleted file mode 100644 index 1090137016b..00000000000 --- a/app/assets/stylesheets/active_admin/_base.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/tables"; -@import "active_admin/components/batch_actions"; -@import "active_admin/components/modal_dialog"; -@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/components/unsupported_browser"; -@import "active_admin/components/tabs"; -@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: 1.5; - font-size: 72%; - background: $body-background-color; - color: $text-color; -} diff --git a/app/assets/stylesheets/active_admin/_forms.scss b/app/assets/stylesheets/active_admin/_forms.scss deleted file mode 100644 index fe5d45e8349..00000000000 --- a/app/assets/stylesheets/active_admin/_forms.scss +++ /dev/null @@ -1,333 +0,0 @@ -//= depend_on_asset "active_admin/datepicker/datepicker-input-icon.png" -// -------------------------------------- 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: $form-label-color; - abbr { - border: none; - color: $required-field-marker-color; - } - } - } - - ol > li.has_many_container { - padding: 20px 10px; - h3 { - font-size: 12px; - font-weight: bold; - } - .has_many_fields { margin: 10px 0 } - } - - ol > li > li label { - line-height:100%; - padding-top:0; - input { - line-height:100%; - vertical-align:middle; - margin-top:-0.1em; - } - } - } - - // relative so the absolutely-positioned handle will fit - .has_many_fields { position: relative } - - .has_many_container { - .handle { - position: absolute; - top: calc(50% - 3em / 2); - right: 2px; - padding: 0; - cursor: move; - } - - // If a sortable is nested in a sortable, give the parent handle space! - &.ui-sortable .has_many_container { - margin-right: 2em; - } - } - - .ui-sortable { - // give the handle space! - input[type=text], input[type=password], input[type=email], input[type=number], input[type=url], input[type=tel], textarea { - width: calc(80% - #{$text-input-total-padding} - 2em - 1px); - } - } - - /* 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; } - } - - &:not(.has_many_fields) ol { - float: left; - width: 74%; - margin: 0; - padding: 0 0 0 20%; - - li { - padding: 0; - border: 0; - } - } - - &.has_many_fields ol { - float: left; - width: 100%; - margin: 0; - } - } - } - - /* Text Fields */ - input[type=text], - input[type=password], - input[type=email], - input[type=number], - input[type=url], - input[type=tel], - input[type=date], - textarea { - width: calc(80% - #{$text-input-total-padding}); - border: $border-width solid #c9d0d6; - @include rounded; - font-size: 0.95em; - @include sans-family; - outline: none; - padding: 8px $text-input-horizontal-padding 7px; - - &:focus { - border: $border-width solid #99a2aa; - @include shadow(0,0,4px,#99a2aa); - } - } - - input[type=date] { - width: calc(100% - #{$text-input-total-padding}); - } - - 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_select, &.time_select, &.datetime_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; } - } - } - - /* Hidden fields */ - &.hidden { - padding: 0; - } - - /* 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=number], input[type=url], input[type=tel], textarea { - border: $border-width 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], input[type=button], button { - @include dark-button; - cursor: pointer; - - } - - .buttons, .actions { - margin-top: 15px; - input[type=submit], input[type=button], button { margin-right: 10px; } - } - - fieldset.buttons li, fieldset.actions li { - float:left; - padding: 0; - - &.cancel a { @include light-button; } - } -} - -// -------------------------------------- Sidebar Forms - -$sidebar-inner-content-width: $sidebar-width - ($section-padding * 2); - -.sidebar_section { - - label { - display: block; - text-transform: uppercase; - color: $form-label-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; } - fieldset { - margin-bottom: 0px; - padding-bottom: 0px; - } - .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 image-url('active_admin/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.scss b/app/assets/stylesheets/active_admin/_header.scss deleted file mode 100644 index 1dd9d68eff3..00000000000 --- a/app/assets/stylesheets/active_admin/_header.scss +++ /dev/null @@ -1,156 +0,0 @@ -//= depend_on_asset "active_admin/nested_menu_arrow.gif" -//= depend_on_asset "active_admin/nested_menu_arrow_dark.gif" -// ----------------------------------- 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: $page-header-text-color; - 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: $page-header-text-color; } - - .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: image-url('active_admin/nested_menu_arrow.gif') no-repeat calc(100% - 7px) 50%; - padding-right: 20px; - } - - &.has_nested.current > a { - background: $current-menu-item-background image-url('active_admin/nested_menu_arrow_dark.gif') no-repeat calc(100% - 7px) 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 image-url('active_admin/nested_menu_arrow_dark.gif') no-repeat calc(100% - 7px) 50%; - z-index: 1020; - } - - - /* 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: 120%; - min-width: 175px; - max-width: calc(100% + 20px); - 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.scss b/app/assets/stylesheets/active_admin/_mixins.scss deleted file mode 100644 index a82c92adf52..00000000000 --- a/app/assets/stylesheets/active_admin/_mixins.scss +++ /dev/null @@ -1 +0,0 @@ -@import "active_admin/mixins/all"; diff --git a/app/assets/stylesheets/active_admin/_typography.scss b/app/assets/stylesheets/active_admin/_typography.scss deleted file mode 100644 index ddb369f37fb..00000000000 --- a/app/assets/stylesheets/active_admin/_typography.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.scss b/app/assets/stylesheets/active_admin/components/_batch_actions.scss deleted file mode 100644 index 12ad1517474..00000000000 --- a/app/assets/stylesheets/active_admin/components/_batch_actions.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 7cd17638a28..00000000000 --- a/app/assets/stylesheets/active_admin/components/_blank_slates.scss +++ /dev/null @@ -1,30 +0,0 @@ -.blank_slate_container { - clear: both; - text-align: center; - - .blank_slate { - @include rounded; - border: $blank-slate-border; - color: $blank-slate-primary-color; - 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 b339907ff8e..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: $breadcrumbs-color; - text-decoration: none; - } - - a:hover { text-decoration: underline; } - - .breadcrumb_sep { - margin: 0 2px; - color: $breadcrumbs-separator-color; - } -} 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 94c1ee18750..00000000000 --- a/app/assets/stylesheets/active_admin/components/_buttons.scss +++ /dev/null @@ -1,6 +0,0 @@ -a.member_link { - margin-right: 7px; - white-space: nowrap; -} - -a.button, a:link.button, a:visited.button, input[type=submit], input[type=button], button { @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.scss b/app/assets/stylesheets/active_admin/components/_comments.scss deleted file mode 100644 index f8862670e30..00000000000 --- a/app/assets/stylesheets/active_admin/components/_comments.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.scss b/app/assets/stylesheets/active_admin/components/_date_picker.scss deleted file mode 100644 index 185fa5b9d7b..00000000000 --- a/app/assets/stylesheets/active_admin/components/_date_picker.scss +++ /dev/null @@ -1,149 +0,0 @@ -// -------------------------------------- Date Picker -.ui-datepicker { - background: #fff; - background-clip: padding-box; - color: #fff; - display: none; - margin-top: 2px; - padding: 0; - text-align: center; - width: 160px; - - a { - text-decoration: none; - &:hover { - cursor: pointer; - } - } - - .ui-datepicker-header { - height: 14px; - @include primary-gradient; - padding: 12px 5px 7px 4px; - margin: 0px 0px 2px 2px; - width: 147px; - border-top-left-radius: 7px; - border-top-right-radius: 7px; - position: relative; - z-index: 2000; - - &:before { - content: ""; - position: absolute; - right: 45%; - top: -6px; - width: 0px; - height: 0px; - border-left: 8.5px solid rgba(0, 0, 0, 0); - border-right: 8.5px solid rgba(0, 0, 0, 0); - border-bottom: 10px solid #676e73; - } - - .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; - - .ui-datepicker-month { - margin: -4px 0 0 0; - } - .ui-datepicker-year { - margin: -4px 0 0 0; - } - } - - - a { - color: #fff; - display: block; - height: 19px; - margin-top: -4px; - width: 10px; - - &.ui-datepicker-prev { - float: left; - width: 0; - height: 0; - margin: 0px 0px 0px 4px; - border-top: 5px solid transparent; - border-right: 5px solid white; - border-bottom: 5px solid transparent; - } - &.ui-datepicker-next { - float: right; - width: 0; - height: 0; - margin: 0px 4px 0px 0px; - border-top: 5px solid transparent; - border-left: 5px solid white; - border-bottom: 5px solid transparent; - } - - 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 dc417f4fab3..00000000000 --- a/app/assets/stylesheets/active_admin/components/_dropdown_menu.scss +++ /dev/null @@ -1,152 +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; - z-index: 2000; - display: none; - - .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; - box-sizing: border-box; - - a { - display: block; - box-sizing: padding-box; - font-size: 0.95em; - font-weight: bold; - padding: 7px 16px 5px; - text-decoration: none; - text-align: center; - white-space: nowrap; - - &: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.scss b/app/assets/stylesheets/active_admin/components/_flash_messages.scss deleted file mode 100644 index 53e73f57d32..00000000000 --- a/app/assets/stylesheets/active_admin/components/_flash_messages.scss +++ /dev/null @@ -1,37 +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; - padding: 13px 30px 11px; - position: relative; - - &.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; - padding: 0; - margin-bottom: 8px; - } -} 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/_modal_dialog.scss b/app/assets/stylesheets/active_admin/components/_modal_dialog.scss deleted file mode 100644 index f3bd4971f24..00000000000 --- a/app/assets/stylesheets/active_admin/components/_modal_dialog.scss +++ /dev/null @@ -1,34 +0,0 @@ -.ui-widget-overlay { - position: fixed; - background: rgba(0,0,0,.2); - top: 0; left: 0; right: 0; bottom: 0; - z-index: 1001; -} - -.ui-dialog { - position: fixed; - z-index: 1002; - @include section-background; - box-shadow: rgba(0,0,0,0.5) 0 0 10px; - - .ui-dialog-titlebar { - @include section-header; - span { font-size: 1.1em } - } - - ul { list-style-type: none } - li { margin: 10px 0 } - label { margin-right: 10px } - - .ui-dialog-buttonpane, form { - padding: 7px 15px 13px - } - .ui-dialog-buttonpane button { - & { @include dark-button } // OK - &:last-child { @include light-button } // Cancel - } -} - -.active_admin_dialog.ui-dialog { - .ui-dialog-titlebar-close { display: 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 ad91730b128..00000000000 --- a/app/assets/stylesheets/active_admin/components/_pagination.scss +++ /dev/null @@ -1,44 +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; -} - -.pagination_per_page { - float: right; - margin-left: 4px; - select { - @include light-button; - @include rounded(0px); - padding: 1px 5px; - } -} 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/_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 77b02e3d67b..00000000000 --- a/app/assets/stylesheets/active_admin/components/_status_tags.scss +++ /dev/null @@ -1,16 +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; } - - &.yes { background: #6090DB } - &.no { background: grey } - -} diff --git a/app/assets/stylesheets/active_admin/components/_table_tools.scss b/app/assets/stylesheets/active_admin/components/_table_tools.scss deleted file mode 100644 index 1b5e430c9a4..00000000000 --- a/app/assets/stylesheets/active_admin/components/_table_tools.scss +++ /dev/null @@ -1,67 +0,0 @@ -.table_tools { - @include clearfix; - margin-bottom: 16px; -} - -.table_tools .dropdown_menu { - float: left; -} - -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; - padding: 4px 14px 4px; - margin: 0; - - &: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 { - list-style-type: none; - padding: 0; - margin: 0; - - li { - float: left; - - a { - border-width: 1px .5px 1px .5px; - border-radius: 0; - } - - &:first-child a { - border-left-width: 1px; - border-top-left-radius: 12px; - border-bottom-left-radius: 12px; - } - - &:last-child a { - border-right-width: 1px; - 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.scss b/app/assets/stylesheets/active_admin/components/_tables.scss deleted file mode 100644 index 29bd4337d4f..00000000000 --- a/app/assets/stylesheets/active_admin/components/_tables.scss +++ /dev/null @@ -1,110 +0,0 @@ -//= depend_on_asset "active_admin/orderable.png" -// ----------------------------------- 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; - } - - &.sortable a { - background: image-url('active_admin/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 { - col.even { background: $table-stripe-color; } - col.odd { background: darken($table-stripe-color, 3%); } - 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/components/_tabs.scss b/app/assets/stylesheets/active_admin/components/_tabs.scss deleted file mode 100644 index a79818d1432..00000000000 --- a/app/assets/stylesheets/active_admin/components/_tabs.scss +++ /dev/null @@ -1,65 +0,0 @@ -.ui-tabs-nav { - list-style: none; - display: block; - width: auto; - margin-bottom: -12px; - padding-left: 0; - overflow: auto; - margin-left: 15px; - - li { - display: block; - position: relative; - margin: 0; - padding: 0; - float: left; - - &:first-child a { - border-left-width: 1px; - border-top-left-radius: 12px; - border-bottom-left-radius: 12px; - } - - &:last-child a { - border-right-width: 1px; - border-top-right-radius: 12px; - border-bottom-right-radius: 12px; - } - - a { - @include light-button; - @include gradient(#FFFFFF, #F0F0F0); - @include border-colors(#d9d9d9, #d0d0d0, #c5c5c5); - text-decoration: none; - border-radius: 0; - border-width: 1px .5px 1px .5px; - margin-right: 0; - padding: 4px 14px 4px; - - &:not(.disabled) { - &:hover { - @include gradient(#FFFFFF, #F6F6F6); - } - } - } - - &.ui-tabs-active { - a { - cursor: default; - @include gradient(#F0F0F0, #FDFDFD); - box-shadow: 0 1px 1px 0 rgba(0,0,0,0.1) inset; - - a:hover { - @include gradient(#F0F0F0, #FDFDFD); - } - } - } - } -} - -.tab-content { - border: 1px solid #D3D3D3; - padding: 15px; - padding-top: 30px; - text-align: left; -} \ No newline at end of file diff --git a/app/assets/stylesheets/active_admin/components/_unsupported_browser.scss b/app/assets/stylesheets/active_admin/components/_unsupported_browser.scss deleted file mode 100644 index 7f6f10731c1..00000000000 --- a/app/assets/stylesheets/active_admin/components/_unsupported_browser.scss +++ /dev/null @@ -1,16 +0,0 @@ -.unsupported_browser { - padding: 10px 30px; - color: #211e14; - background-color: #fae692; - @include gradient(#feefae, #fae692); - border-bottom: 1px solid #b3a569; - - h1 { - font-size: 13px; - font-weight: bold; - } - - p { - margin-bottom: 0.5em; - } -} diff --git a/app/assets/stylesheets/active_admin/mixins/_all.scss b/app/assets/stylesheets/active_admin/mixins/_all.scss deleted file mode 100644 index 81a65a04076..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_all.scss +++ /dev/null @@ -1,10 +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/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.scss b/app/assets/stylesheets/active_admin/mixins/_buttons.scss deleted file mode 100644 index efb9462d66d..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_buttons.scss +++ /dev/null @@ -1,65 +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; - - &.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.scss b/app/assets/stylesheets/active_admin/mixins/_gradients.scss deleted file mode 100644 index 1bf3777a485..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_gradients.scss +++ /dev/null @@ -1,28 +0,0 @@ -$secondary-gradient-start: #efefef !default; -$secondary-gradient-stop: #dfe1e2 !default; - -@mixin gradient($start, $end){ - background-color: $start; - background-image: linear-gradient(180deg, $start, $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-color: none; -} diff --git a/app/assets/stylesheets/active_admin/mixins/_reset.scss b/app/assets/stylesheets/active_admin/mixins/_reset.scss deleted file mode 100644 index 7b89b89b278..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_reset.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.scss b/app/assets/stylesheets/active_admin/mixins/_rounded.scss deleted file mode 100644 index 4e8201d4f62..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_rounded.scss +++ /dev/null @@ -1,22 +0,0 @@ -@mixin rounded($radius: 3px) { - border-radius: $radius; -} - -@mixin rounded-all($top-left:3px, $top-right:3px, $bottom-right:3px, $bottom-left:3px) { - border-top-right-radius: $top-right; - border-top-left-radius: $top-left; - border-bottom-right-radius: $bottom-right; - border-bottom-left-radius: $bottom-left; -} - -@mixin rounded-top($radius: 3px) { - @include rounded(0); - border-top-right-radius: $radius; - border-top-left-radius: $radius; -} - -@mixin rounded-bottom($radius: 3px) { - @include rounded(0); - border-bottom-right-radius: $radius; - border-bottom-left-radius: $radius; -} diff --git a/app/assets/stylesheets/active_admin/mixins/_sections.scss b/app/assets/stylesheets/active_admin/mixins/_sections.scss deleted file mode 100644 index f34d78b0639..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_sections.scss +++ /dev/null @@ -1,41 +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: 1em; - font-weight: bold; - line-height: 18px; - margin-bottom: 0.5em; - color: $section-header-text-color; - - 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; - - .header_action { - float: right; - } - } - - > 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.scss b/app/assets/stylesheets/active_admin/mixins/_shadows.scss deleted file mode 100644 index e366bf0ad46..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_shadows.scss +++ /dev/null @@ -1,15 +0,0 @@ -@mixin shadow($x: 0, $y: 1px, $blur: 2px, $color: rgba(0,0,0,0.37)) { - box-shadow: $x $y $blur $color; -} - -@mixin no-shadow { - box-shadow: none; -} - -@mixin inset-shadow($x: 0, $y: 1px, $blur: 2px, $color: #aaa) { - 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 8ec668216ff..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_typography.scss +++ /dev/null @@ -1,3 +0,0 @@ -@mixin sans-family { - font-family: 'Helvetica Neue', 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 bf42a8d2d08..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_utilities.scss +++ /dev/null @@ -1,17 +0,0 @@ -@mixin clearfix { - &:after { - visibility: hidden; - display: block; - content: ""; - clear: both; - height: 0; - } -} - -@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.scss b/app/assets/stylesheets/active_admin/mixins/_variables.scss deleted file mode 100644 index 5cf62a94c95..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_variables.scss +++ /dev/null @@ -1,34 +0,0 @@ -// Variables used throughout Active Admin. -// They can be overridden by prepending your own -// to 'app/assets/stylesheets/active_admin.scss'. - -// Colors -$body-background-color: #FFF !default; -$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; -$blank-slate-primary-color: #AAA !default; -$breadcrumbs-color: #8a949e !default; -$breadcrumbs-separator-color: #aab2ba !default; -$required-field-marker-color: #aaa !default; -$form-label-color: $section-header-text-color !default; -$page-header-text-color: #cdcdcd !default; - -// Sizes -$border-width: 1px !default; -$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; -$text-input-total-padding: $text-input-horizontal-padding * 2 + $border-width * 2; - -$blank-slate-border: 1px dashed #DADADA !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.scss b/app/assets/stylesheets/active_admin/print.scss deleted file mode 100644 index 338007cfef6..00000000000 --- a/app/assets/stylesheets/active_admin/print.scss +++ /dev/null @@ -1,288 +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; - } - - } - } -} - -.unsupported_browser { - 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 c49e3ae86fc..00000000000 --- a/app/assets/stylesheets/active_admin/structure/_main_structure.scss +++ /dev/null @@ -1,29 +0,0 @@ -#wrapper { - width: 100%; -} - -.index #wrapper { - display: table; -} - -#active_admin_content { - margin: 0; - padding: $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 61b634c75e1..00000000000 --- a/app/assets/stylesheets/active_admin/structure/_title_bar.scss +++ /dev/null @@ -1,41 +0,0 @@ -#title_bar { - @include section-header; - @include clearfix; - 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; - 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; - line-height: 100%; - font-weight: bold; - } - - .action_items { - span.action_item { - & > a, & > .dropdown_menu > a { - @include light-button; - padding: 12px 17px 10px; - margin: 0px; - } - } - } -} diff --git a/lib/active_admin/base_controller.rb b/app/controllers/active_admin/base_controller.rb similarity index 51% rename from lib/active_admin/base_controller.rb rename to app/controllers/active_admin/base_controller.rb index 50be2163c62..c2dd73ffaa0 100644 --- a/lib/active_admin/base_controller.rb +++ b/app/controllers/active_admin/base_controller.rb @@ -1,17 +1,20 @@ -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_method :env + 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 @@ -22,6 +25,11 @@ class << self attr_accessor :active_admin_config end + 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. @@ -29,11 +37,6 @@ 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 @@ -59,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/lib/active_admin/base_controller/authorization.rb b/app/controllers/active_admin/base_controller/authorization.rb similarity index 72% rename from lib/active_admin/base_controller/authorization.rb rename to app/controllers/active_admin/base_controller/authorization.rb index de298cc5275..f4c036f8f39 100644 --- a/lib/active_admin/base_controller/authorization.rb +++ b/app/controllers/active_admin/base_controller/authorization.rb @@ -1,16 +1,16 @@ +# frozen_string_literal: true module ActiveAdmin 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, + 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 } @@ -19,6 +19,7 @@ module Authorization helper_method :authorized? helper_method :authorize! + helper_method :active_admin_authorization end protected @@ -38,9 +39,8 @@ 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 + # 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 @@ -53,9 +53,10 @@ def authorized?(action, subject = nil) # an ActiveAdmin::AccessDenied. def authorize!(action, subject = nil) unless authorized? action, subject - raise ActiveAdmin::AccessDenied.new(current_active_admin_user, - action, - subject) + raise ActiveAdmin::AccessDenied.new( + current_active_admin_user, + action, + subject) end end @@ -81,7 +82,7 @@ def active_admin_authorization def active_admin_authorization_adapter adapter = active_admin_namespace.authorization_adapter if adapter.is_a? String - ActiveSupport::Dependencies.constantize adapter + adapter.constantize else adapter end @@ -101,7 +102,7 @@ def action_to_permission(action) end def dispatch_active_admin_access_denied(exception) - call_method_or_exec_proc active_admin_namespace.on_unauthorized_access, exception + instance_exec(self, exception, &active_admin_namespace.on_unauthorized_access.to_proc) end def rescue_active_admin_access_denied(exception) @@ -113,19 +114,14 @@ def rescue_active_admin_access_denied(exception) redirect_backwards_or_to_root end - format.csv { render text: error, status: :unauthorized } - format.json { render json: { error: error }, status: :unauthorized } - format.xml { render xml: "#{error}", status: :unauthorized } + 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 - if request.headers.key? "HTTP_REFERER" - redirect_to :back - else - controller, action = active_admin_namespace.root_to.split '#' - redirect_to controller: controller, action: action - end + redirect_back fallback_location: active_admin_root 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/lib/active_admin/resource_controller/data_access.rb b/app/controllers/active_admin/resource_controller/data_access.rb similarity index 79% rename from lib/active_admin/resource_controller/data_access.rb rename to app/controllers/active_admin/resource_controller/data_access.rb index 0c2306cb7e2..18f08b53cff 100644 --- a/lib/active_admin/resource_controller/data_access.rb +++ b/app/controllers/active_admin/resource_controller/data_access.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin class ResourceController < BaseController @@ -14,6 +15,8 @@ def self.included(base) include ScopeChain define_active_admin_callbacks :build, :create, :update, :save, :destroy + + helper_method :current_scope end end @@ -21,9 +24,9 @@ def self.included(base) COLLECTION_APPLIES = [ :authorization_scope, - :sorting, :filtering, :scoping, + :sorting, :includes, :pagination, :collection_decorator @@ -45,7 +48,6 @@ def 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 @@ -55,12 +57,11 @@ def collection def find_collection(options = {}) collection = scoped_collection collection_applies(options).each do |applyer| - collection = send("apply_#{applyer}", collection) + collection = send(:"apply_#{applyer}", collection) end collection end - # Override this method in your controllers to modify the start point # of our searches and index. # @@ -98,16 +99,11 @@ def resource # Does the actual work of finding a resource in the database. This # method uses the finder method as defined in InheritedResources. # - # Note that public_send can't be used here because Rails 3.2's - # ActiveRecord::Associations::CollectionProxy (belongs_to associations) - # mysteriously returns an Enumerator object. - # # @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. @@ -131,12 +127,12 @@ def build_resource # Builds a new resource. This method uses the method_for_build provided # by Inherited Resources. # - # Note that public_send can't be used here w/ Rails 3.2 & a belongs_to - # config, or you'll get undefined method `build' for []:Array. - # # @return [ActiveRecord::Base] An un-saved active record base object def build_new_resource - scoped_collection.send method_for_build + 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. @@ -172,11 +168,16 @@ def save_resource(object) # # @return [void] def update_resource(object, attributes) - object = assign_attributes(object, attributes) - - run_update_callbacks object do - save_resource(object) + 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. @@ -188,12 +189,10 @@ def destroy_resource(object) 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 @@ -209,11 +208,10 @@ def apply_authorization_scope(collection) def apply_sorting(chain) params[:order] ||= active_admin_config.sort_order - - order_clause = OrderClause.new params[:order] + order_clause = active_admin_config.order_clause.new(active_admin_config, params[:order]) if order_clause.valid? - chain.reorder(order_clause.to_sql(active_admin_config)) + order_clause.apply(chain) else chain # just return the chain end @@ -222,18 +220,10 @@ def apply_sorting(chain) # 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 clean_search_params params[:q] + @search = chain.ransack(params[:q] || {}, auth_object: active_admin_authorization) @search.result end - def clean_search_params(params) - if params.is_a? Hash - params.dup.delete_if{ |key, value| value.blank? } - else - {} - end - end - def apply_scoping(chain) @collection_before_scope = chain @@ -246,7 +236,7 @@ def apply_scoping(chain) def apply_includes(chain) if active_admin_config.includes.any? - chain.includes *active_admin_config.includes + chain.includes(*active_admin_config.includes) else chain end @@ -258,26 +248,37 @@ def collection_before_scope 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 + active_admin_config.get_scope_by_id(params[:scope]) + else + active_admin_config.default_scope(self) + end end def apply_pagination(chain) - page_method_name = Kaminari.config.page_method_name + # 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] - chain.public_send(page_method_name, page).per(per_page) + paginate(chain, page, per_page) end def collection_applies(options = {}) only = Array(options.fetch(:only, COLLECTION_APPLIES)) except = Array(options.fetch(:except, [])) - # see #4074 for code reasons - COLLECTION_APPLIES.select { |applier| only.include? applier } - .reject { |applier| except.include? applier } + 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 @@ -316,6 +317,37 @@ def assign_attributes(resource, attributes) def apply_decorations(resource) apply_decorator(resource) end + + # @return [String] + def smart_resource_url + if create_another? + new_resource_url(create_another: params[:create_another]) + else + super + end + end + + private + + # @return [Boolean] true if user requested to create one more + # resource after creating this one. + def create_another? + params[:create_another].present? + end + + def paginated_collection(page_no = 1) + paginate(collection, page_no, batch_size) + end + + def paginate(chain, page, per_page) + page_method_name = Kaminari.config.page_method_name + + chain.public_send(page_method_name, page).per(per_page) + end + + def batch_size + 1000 + end end end end diff --git a/lib/active_admin/resource_controller/decorators.rb b/app/controllers/active_admin/resource_controller/decorators.rb similarity index 63% rename from lib/active_admin/resource_controller/decorators.rb rename to app/controllers/active_admin/resource_controller/decorators.rb index 33fff8c531b..a916cb24b12 100644 --- a/lib/active_admin/resource_controller/decorators.rb +++ b/app/controllers/active_admin/resource_controller/decorators.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true module ActiveAdmin class ResourceController < BaseController module Decorators - protected + protected def apply_decorator(resource) decorate? ? decorator_class.new(resource) : resource @@ -24,11 +25,11 @@ def self.undecorate(resource) end end - private + private def decorate? case action_name - when 'new', 'edit' + when "new", "edit", "create", "update" form = active_admin_config.get_page_presenter :form form && form.options[:decorate] && decorator_class.present? else @@ -53,49 +54,27 @@ class Wrapper def self.wrap(decorator) collection_decorator = find_collection_decorator(decorator) - - if draper_collection_decorator? collection_decorator - name = "#{collection_decorator.name} of #{decorator} + ActiveAdmin" - @cache[name] ||= wrap! collection_decorator, name - else - collection_decorator - end + name = "#{collection_decorator.name} of #{decorator} + ActiveAdmin" + @cache[name] ||= wrap! collection_decorator, name end - private - def self.wrap!(parent, name) ::Class.new parent do delegate :reorder, :page, :current_page, :total_pages, :limit_value, - :total_count, :num_pages, :to_key, :group_values, :except, - :find_each, :ransack + :total_count, :offset, :to_key, :group_values, + :except, :find_each, :ransack, to: :object define_singleton_method(:name) { name } end end - # Draper::CollectionDecorator was introduced in 1.0.0 - # Draper::Decorator#collection_decorator_class was introduced in 1.3.0 def self.find_collection_decorator(decorator) - if Dependency.draper? '>= 1.3.0' + if decorator.respond_to?(:collection_decorator_class) decorator.collection_decorator_class - elsif Dependency.draper? '>= 1.0.0' - draper_collection_decorator else - decorator + CollectionDecorator end end - - def self.draper_collection_decorator?(decorator) - decorator && decorator <= draper_collection_decorator - rescue NameError - false - end - - def self.draper_collection_decorator - ::Draper::CollectionDecorator - end - end end end diff --git a/app/controllers/active_admin/resource_controller/polymorphic_routes.rb b/app/controllers/active_admin/resource_controller/polymorphic_routes.rb new file mode 100644 index 00000000000..f217024d954 --- /dev/null +++ b/app/controllers/active_admin/resource_controller/polymorphic_routes.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +require "active_admin/resource" +require "active_admin/resource/model" + +module ActiveAdmin + class ResourceController < BaseController + module PolymorphicRoutes + def polymorphic_url(record_or_hash_or_array, options = {}) + super(map_named_resources_for(record_or_hash_or_array), options) + end + + def polymorphic_path(record_or_hash_or_array, options = {}) + super(map_named_resources_for(record_or_hash_or_array), options) + end + + private + + def map_named_resources_for(record_or_hash_or_array) + return record_or_hash_or_array unless record_or_hash_or_array.is_a?(Array) + + record_or_hash_or_array.map { |record| to_named_resource(record) } + end + + def to_named_resource(record) + if record.is_a?(resource_class) + return ActiveAdmin::Model.new(active_admin_config, record) + end + + belongs_to_resource = active_admin_config.belongs_to_config.try(:resource) + if belongs_to_resource && record.is_a?(belongs_to_resource.resource_class) + return ActiveAdmin::Model.new(belongs_to_resource, record) + end + + record + end + end + end +end diff --git a/lib/active_admin/resource_controller/resource_class_methods.rb b/app/controllers/active_admin/resource_controller/resource_class_methods.rb similarity index 92% rename from lib/active_admin/resource_controller/resource_class_methods.rb rename to app/controllers/active_admin/resource_controller/resource_class_methods.rb index 1232910146a..72f361edb98 100644 --- a/lib/active_admin/resource_controller/resource_class_methods.rb +++ b/app/controllers/active_admin/resource_controller/resource_class_methods.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin class ResourceController < BaseController module ResourceClassMethods @@ -13,6 +14,8 @@ def self.resource_class @active_admin_config ? @active_admin_config.resource_class : nil end + private + def resource_class self.class.resource_class end diff --git a/lib/active_admin/resource_controller/scoping.rb b/app/controllers/active_admin/resource_controller/scoping.rb similarity index 89% rename from lib/active_admin/resource_controller/scoping.rb rename to app/controllers/active_admin/resource_controller/scoping.rb index 30c03234f20..19e97206567 100644 --- a/lib/active_admin/resource_controller/scoping.rb +++ b/app/controllers/active_admin/resource_controller/scoping.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin class ResourceController < BaseController @@ -15,7 +16,7 @@ module Scoping # Collection can be scoped conditionally with an :if or :unless proc. def begin_of_association_chain return nil unless active_admin_config.scope_to?(self) - render_in_context(self, active_admin_config.scope_to_method) + helpers.render_in_context(self, active_admin_config.scope_to_method) end # Overriding from InheritedResources::BaseHelpers diff --git a/app/controllers/active_admin/resource_controller/streaming.rb b/app/controllers/active_admin/resource_controller/streaming.rb new file mode 100644 index 00000000000..ed3806964ab --- /dev/null +++ b/app/controllers/active_admin/resource_controller/streaming.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true +require "csv" + +module ActiveAdmin + class ResourceController < BaseController + + # This module overrides CSV responses to allow large data downloads. + # Could be expanded to JSON and XML in the future. + # + module Streaming + + def index + super do |format| + format.csv { stream_csv } + yield(format) if block_given? + end + end + + protected + + def stream_resource(&block) + headers["X-Accel-Buffering"] = "no" + headers["Cache-Control"] = "no-cache" + headers["Last-Modified"] = Time.current.httpdate + + if ActiveAdmin.application.disable_streaming_in.include? Rails.env + self.response_body = block[String.new] # rubocop:disable Performance/UnfreezeString to preserve encoding + else + self.response_body = Enumerator.new(&block) + end + end + + def csv_filename + "#{resource_collection_name.to_s.tr('_', '-')}-#{Time.zone.now.to_date}.csv" + end + + def stream_csv + headers["Content-Type"] = "text/csv; charset=utf-8" # In Rails 5 it's set to HTML?? + headers["Content-Disposition"] = %{attachment; filename="#{csv_filename}"} + stream_resource(&active_admin_config.csv_builder.method(:build).to_proc.curry[self]) + end + + end + end +end diff --git a/app/helpers/active_admin/auto_link_helper.rb b/app/helpers/active_admin/auto_link_helper.rb new file mode 100644 index 00000000000..ed3e7b823cb --- /dev/null +++ b/app/helpers/active_admin/auto_link_helper.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true +module ActiveAdmin + 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::DisplayHelper#display_name + # + # You can pass in the content to display + # eg: auto_link(@post, "My Link") + # + def auto_link(resource, content = display_name(resource), **html_options) + if url = auto_url_for(resource) + link_to content, url, html_options + else + content + end + end + + # Like `auto_link`, except that it only returns a URL for the resource + def auto_url_for(resource) + config = active_admin_resource_for(resource.class) + return unless config + + if config.controller.action_methods.include?("show") && + authorized?(ActiveAdmin::Auth::READ, resource) + url_for config.route_instance_path resource, url_options + elsif config.controller.action_methods.include?("edit") && + authorized?(ActiveAdmin::Auth::EDIT, resource) + url_for config.route_edit_instance_path resource, url_options + end + end + + def new_action_authorized?(resource_or_class) + controller.action_methods.include?("new") && authorized?(ActiveAdmin::Auth::NEW, resource_or_class) + end + + def show_action_authorized?(resource_or_class) + controller.action_methods.include?("show") && authorized?(ActiveAdmin::Auth::READ, resource_or_class) + end + + def edit_action_authorized?(resource_or_class) + controller.action_methods.include?("edit") && authorized?(ActiveAdmin::Auth::EDIT, resource_or_class) + end + + def destroy_action_authorized?(resource_or_class) + controller.action_methods.include?("destroy") && authorized?(ActiveAdmin::Auth::DESTROY, resource_or_class) + end + + def auto_logout_link_path + render_or_call_method_or_proc_on(self, active_admin_namespace.logout_link_path) + end + + private + + # Returns the ActiveAdmin::Resource instance for a class + # While `active_admin_namespace` is a helper method, this method seems + # to exist to otherwise resolve failed component specs using mock_action_view. + def active_admin_resource_for(klass) + if respond_to? :active_admin_namespace + active_admin_namespace.resource_for klass + end + end + end +end diff --git a/app/helpers/active_admin/breadcrumb_helper.rb b/app/helpers/active_admin/breadcrumb_helper.rb new file mode 100644 index 00000000000..c7fbd209b96 --- /dev/null +++ b/app/helpers/active_admin/breadcrumb_helper.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true +module ActiveAdmin + module BreadcrumbHelper + ID_FORMAT_REGEXP = /\A(\d+|[a-f0-9]{24}|(?:[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}))\z/.freeze + + # Returns an array of links to use in a breadcrumb + def build_breadcrumb_links(path = request.path, html_options = {}) + config = active_admin_config.breadcrumb + if config.is_a?(Proc) + instance_exec(controller, &config) + elsif config.present? + default_breadcrumb_links(path, html_options) + end + end + + def default_breadcrumb_links(path, html_options = {}) + # remove leading "/" and split up the URL + # and remove last since it's used as the page title + parts = path.split("/").select(&:present?)[0..-2] + + 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 ID_FORMAT_REGEXP.match?(part) && parts[index - 1] + parent = active_admin_config.belongs_to_config.try :target + config = parent && parent.resource_name.route_key == parts[index - 1] ? parent : active_admin_config + name = display_name config.find_resource part + end + name ||= I18n.t "activerecord.models.#{part.singularize}", count: 2.1, default: part.titlecase + + # Don't create a link if the resource's show action is disabled + if !config || config.defined_actions.include?(:show) + link_to name, "/" + parts[0..index].join("/"), html_options + else + name + end + end + end + end +end diff --git a/app/helpers/active_admin/display_helper.rb b/app/helpers/active_admin/display_helper.rb new file mode 100644 index 00000000000..2e2b87575f5 --- /dev/null +++ b/app/helpers/active_admin/display_helper.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true +module ActiveAdmin + module DisplayHelper + DISPLAY_NAME_FALLBACK = -> { + 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..cb50ac5fb1a --- /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: "-") + "#{@page_title || page_title} #{separator} #{site_title}" + 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..ded5fbfd8b4 --- /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/devise/confirmations/new.html.erb b/app/views/active_admin/devise/confirmations/new.html.erb index 0deba76f0eb..1d83245a901 100644 --- a/app/views/active_admin/devise/confirmations/new.html.erb +++ b/app/views/active_admin/devise/confirmations/new.html.erb @@ -1,13 +1,15 @@ -
-

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

+
+

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

- <%= devise_error_messages! %> + <%= 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: { value: t('active_admin.devise.resend_confirmation_instructions.submit') } + 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 %> 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 e1144e943b4..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 0429883f05b..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 31e185e41d1..4703627920e 100644 --- a/app/views/active_admin/devise/passwords/edit.html.erb +++ b/app/views/active_admin/devise/passwords/edit.html.erb @@ -1,7 +1,9 @@ -
-

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

+
+

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

- <%= 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 @@ -9,7 +11,7 @@ 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.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 %> diff --git a/app/views/active_admin/devise/passwords/new.html.erb b/app/views/active_admin/devise/passwords/new.html.erb index df5c8b2b99c..1c03cd706d9 100644 --- a/app/views/active_admin/devise/passwords/new.html.erb +++ b/app/views/active_admin/devise/passwords/new.html.erb @@ -1,13 +1,14 @@ -
-

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

+
+

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

- <%= devise_error_messages! %> <%= 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 %> diff --git a/app/views/active_admin/devise/registrations/new.html.erb b/app/views/active_admin/devise/registrations/new.html.erb index 899c1f4881f..a6c4a55a619 100644 --- a/app/views/active_admin/devise/registrations/new.html.erb +++ b/app/views/active_admin/devise/registrations/new.html.erb @@ -1,22 +1,23 @@ -
-

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

+
+

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

<% scope = Devise::Mapping.find_scope!(resource_name) %> - <%= devise_error_messages! %> - <%= active_admin_form_for(resource, as: resource_name, url: send(:"#{scope}_registration_path"), html: { id: "registration_new" }) do |f| + <%= 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, lable: t('active_admin.devise.password_confirmation.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: { value: t('active_admin.devise.sign_up.submit') } + 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 87ab60d50ea..1c1172e8621 100644 --- a/app/views/active_admin/devise/sessions/new.html.erb +++ b/app/views/active_admin/devise/sessions/new.html.erb @@ -1,17 +1,19 @@ -
-

<%= render_or_call_method_or_proc_on(self, active_admin_application.site_title) %> <%= 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_with_index { |key, index| - f.input key, label: t('active_admin.devise.'+key.to_s+'.title'), input_html: { autofocus: index.zero? } + 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 %> 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 9daf5702436..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) %>
+ 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 84ed9518ed1..24050a089be 100644 --- a/app/views/active_admin/devise/unlocks/new.html.erb +++ b/app/views/active_admin/devise/unlocks/new.html.erb @@ -1,15 +1,17 @@ -
-

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

+
+

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

- <%= devise_error_messages! %> + <%= 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.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..8f71651ca0c --- /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), 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 36b8a4c88be..f1211e928bf 100644 --- a/app/views/layouts/active_admin_logged_out.html.erb +++ b/app/views/layouts/active_admin_logged_out.html.erb @@ -1,39 +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 %> - - <%= favicon_link_tag ActiveAdmin.application.favicon if ActiveAdmin.application.favicon %> - - <% ActiveAdmin.application.meta_tags_for_logged_out_pages.each do |name, content| %> - <%= tag(:meta, name: name, content: content) %> - <% end %> - - <%= csrf_meta_tag %> + <%= html_head_site_title %> + <%= render "active_admin/html_head" %> - -
- -
- <% flash_messages.each do |type, message| %> - <%= content_tag :div, message, class: "flash flash_#{type}" %> - <% 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..e403cc20dc0 --- /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 "yarn install --frozen-lockfile" +system "bin/rake", "dependencies:vendor" +system "yarn 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 index b62e6d6824a..fa25d1e5164 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -1,135 +1,148 @@ +--- ar: active_admin: - dashboard: "لوحة تحكم" - dashboard_welcome: - welcome: "مرحبًا بك في في صفحة الإدارة، وهذه هي الصفحة الإفتراضيّة." - 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: "أقل من" - search_status: - headline: "المجال:" - current_filters: "المُرشحات الحاليّة:" - no_current_filters: "بدون" - status_tag: - 'yes': "نعم" - 'no': "لا" - main_content: "الرجاء تنفيذ %{model}#main_content لعرض المحتوى." - logout: "تسجيل الخروج" - powered_by: "تنفيذ %{active_admin} %{version}" - sidebars: - filters: "المُرشحات" - search_status: "حالات البحث" - pagination: - empty: "لا يوجد %{model} " - one: "عرض 1 %{model}" - one_page: "عرض all %{n} %{model}" - multiple: "عرض %{model} %{from} - %{to} من %{total} بالمجمل" - multiple_without_total: "عرض %{model} %{from} - %{to}" - entry: - one: "مدخل" - other: "مدخلات" - any: "أي" - blank_slate: - content: "لايوجد %{resource_name} بعد." - link: "إنشاء" - dropdown_actions: - button_label: "إجراءات" + access_denied: + message: غير مصرح لك تنفيذ هذا الإجراء. + any: أي batch_actions: - button_label: "إجراءات متعددة" - default_confirmation: "هل أنت متأكّد؟" - 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: حُذف بنجاح %{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: "لا يوجد تعليقات بعد." - author_missing: "مجهول" - title_content: "تعليقات (%{count})" + add: إضافة تعليق + author: مؤلّف + author_missing: المؤلف مجهول + author_type: نوع الؤلّف + body: المحتوى + created_at: أُنشئ + delete: حذف تعليق + delete_confirmation: هل أنت متأكّد من حذف التعليق؟ errors: - empty_text: "لم يُحفظ التعليق، النص فارغ." - created_at: "أُنشئ" - delete: "حذف تعليق" - delete_confirmation: "هل أنت متأكّد من حذف هذه التعليقات؟" + empty_text: لم يُحفظ التعليق، النص فارغ. + no_comments_yet: لا يوجد تعليقات. + resource: مدخل + resource_type: نوع المصدر + title_content: التعليقات (%{count}) + create_another: انشاء %{model} آخر + dashboard: لوحة التحكم + delete: حذف + delete_confirmation: هل تريد تأكيد الحذف؟ + delete_model: حذف %{model} + details: تفاصيل %{model} devise: - username: - title: "اسم المستخدم" + change_password: + submit: تغير كلمة المرور + title: تغير كلمة المرور email: - title: "البريد الإلكترونيّ" - subdomain: - title: "مجال فرعي" - password: - title: "كلمة المرور" - sign_up: - title: "تسجيل الدخول" - submit: "تسجيل الدخول" + 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: "إعادة إرسال تعليمات فك الحظر" - resend_confirmation_instructions: - title: "إعادة ارسال تعليمات التأكيد" - submit: "إعادة ارسال تعليمات التأكيد" - links: - sign_up: "التسجيل" - sign_in: "دخول" - forgot_your_password: "هل نسيت كلمة المرور؟" - sign_in_with_omniauth_provider: "تسجيل الدخول بـ %{provider}" - resend_unlock_instructions: "إعادة إرسال تعليمات تنشيط الحساب" - resend_confirmation_instructions: "إعادة إرسال تعليمات تأكيد الحساب" - unsupported_browser: - headline: "يُرجى مُلاحظة أن (أكتف أدمن) لم تعد تدعم المُتصفّح إنترنت اكسبلوررالإصدار الثامن وما قبله" - recommendation: "ننصح بالتحديث إلى الإصدارات الأخيرة من: Internet Explorer, Google Chrome, أو Firefox." - turn_off_compatibility_view: "إن كنت تستخدم الإصدار التاسع وما يليه من إنترنت إكسبلورر تأكّد من تعطيل \"Compatibility View\"." - 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: + 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 addab128ca3..36dd5679e70 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -1,121 +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}" - 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: "по-малко от" - status_tag: - "yes": "Да" - "no": "не" - 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}" - multiple_without_total: "Показване %{model} %{from} - %{to}" - entry: - one: "запис" - other: "записи" - any: "Без значение" - blank_slate: - content: "Все още няма добавени %{resource_name}." - link: "Създаване" - dropdown_actions: - button_label: "действия" + access_denied: + message: Нямате права да извършите това действие. + any: Без значение batch_actions: - button_label: "Масови действия" - default_confirmation: "Наистина ли искате да направите това?" - 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: "Все още няма коментари." - author_missing: "Анонимен" - 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: - username: - title: "Потребителско име" + change_password: + submit: Промяна на паролата + title: Промяна на паролата email: - title: "Поща" - subdomain: - title: "Поддомейн" - password: - title: "Парола" - sign_up: - title: "Регистрация" - submit: "Регистрация" + 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: "Изпрати отново инструкциите за отключване" - resend_confirmation_instructions: - 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 index 42ae0559279..2204cf36b16 100644 --- a/config/locales/bs.yml +++ b/config/locales/bs.yml @@ -1,122 +1,108 @@ +--- bs: 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 biste 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}" - edit_model: "Uredi %{model}" - delete_model: "Obriši %{model}" - details: "%{model} detalji" - cancel: "Odustani" - empty: "Prazno" - previous: "Prethodni" - next: "Sljedeć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: "Ukloni 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" - status_tag: - "yes": "Da" - "no": "Nema" - main_content: "Molim Vas, implementirajte %{model}#main_content da biste 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}" - multiple_without_total: "Prikazani %{model} %{from} - %{to}" - 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" - default_confirmation: "Jeste li sigurni da želite to učiniti?" - delete_confirmation: "Jeste li sigurni da želite obrisati %{plural_model}?" - 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: "(Izmijeni 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: - resource_type: "Tip objekta" - author_type: "Tip autora" - body: "Sadržaj" - author: "Autor" - title: "Komentar" - add: "Dodaj komentar" - resource: "Objekt" - no_comments_yet: "Još nema komentara." - author_missing: "Anoniman" - 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: - username: - title: "Korisničko ime" + change_password: + submit: Izmijeni lozinku + title: Izmjena lozinke email: - title: "Email" - subdomain: - title: "Poddomena" - password: - title: "Lozinka" - sign_up: - title: "Registracija" - submit: "Registruj" + title: Email + links: + forgot_your_password: Zaboravljena lozinka? + sign_in: Prijavi se + sign_in_with_omniauth_provider: Prijavite se za %{provider} login: - title: "Prijava" - remember_me: "Zapamti me" - submit: "Prijavi se" + 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: - title: "Zaboravljena lozinka?" - submit: "Resetuj lozinku" - change_password: - title: "Izmjena lozinke" - submit: "Izmijeni lozinku" + submit: Resetuj lozinku + title: Zaboravljena lozinka? + sign_up: + submit: Registruj + title: Registracija + subdomain: + title: Poddomena unlock: - title: "Ponovno slanje uputstva za otključavanje" - submit: "Pošalji" - resend_confirmation_instructions: - title: "Ponovno slanje uputstva za potvrdu" - submit: "Pošalji" - links: - sign_in: "Prijavi se" - forgot_your_password: "Zaboravljena lozinka?" - sign_in_with_omniauth_provider: "Prijavite se za %{provider}" - access_denied: - message: "Nemaš dopuštenja." + 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" - block: "Lista" - grid: "Rešetka" - blog: "Blog" + 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 d87c1b7366c..e69588f45e3 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -1,107 +1,143 @@ -#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" - status_tag: - "yes": "Sí" - "no": "No" - 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" - multiple_without_total: "S'estan mostrant %{model} %{from} - %{to}" - entry: - one: "entrada" - other: "entrades" - any: "Qualsevol" - blank_slate: - content: "Encara no hi ha cap %{resource_name}." - link: "Crea'n un/a" - dropdown_actions: - button_label: "accions" + access_denied: + message: No esteu autoritzats a realitzar aquesta acció + any: Qualsevol batch_actions: - button_label: "les accions per lots" - default_confirmation: "¿Esteu segur que voleu fer-ho?" - delete_confirmation: "¿Està segur que desitja eliminar aquests %{plural_model}?" - 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" - resend_confirmation_instructions: - title: "Reenviar instruccions de confirmació" - submit: "Reenviar instruccions de confirmació" - 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 + 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í + 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 55cb7290b81..7c1d90c2dea 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -1,110 +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í na nástěnku se podívejte do souboru '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" - edit_model: "Upravit" - delete_model: "Smazat" - details: "Detaily" - cancel: "Zrušit" - empty: "Prázdné" - previous: "Předchozí" - next: "Následující" - download: "Stáhnout:" - has_many_new: "Přidat nový" - has_many_delete: "Smazat" - has_many_remove: "Odstranit" - filters: - buttons: - filter: "Filtrovat" - clear: "Vyčistit filtry" - predicates: - contains: "Obsahuje" - equals: "Odpovídá" - starts_with: "Začíná na" - ends_with: "Končí na" - greater_than: "Větší než" - less_than: "Menší než" - status_tag: - "yes": "Ano" - "no": "Ne" - main_content: "Implementujte prosím %{model}#main_content pro zobrazení obsahu." - logout: "Odhlásit" - 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}" - multiple_without_total: "%{from} - %{to}" - 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" - dropdown_actions: - button_label: "Akce" + access_denied: + message: Nemáte oprávnění k provedení této akce. + any: Kterákoliv batch_actions: - button_label: "Hromadné akce" - default_confirmation: "Jste si jisti, že chcete provést?" - delete_confirmation: "Jste si jisti, že chcete smazat tyto %{plural_model}?" - succesfully_destroyed: - zero: "Nebyl smazán žádný %{model}" - 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: - resource_type: "Typ zdroje" - author_type: "Typ autora" - body: "Tělo" - author: "Autor" - title: "Komentář" - add: "Přidat komentář" - resource: "Zdroj" - no_comments_yet: "Žádný komentář" - author_missing: "Anonymní" - 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: + change_password: + submit: Změnit své heslo + title: Změnit heslo + links: + 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: - title: "Přihlášení" - remember_me: "Zapamatovat si mě" - submit: "Přihlásit" + remember_me: Zapamatovat si mě + submit: Přihlásit + title: Přihlášení reset_password: - title: "Zapomněli jste heslo?" - submit: "Obnovit heslo" - change_password: - title: "Změnit heslo" - submit: "Změnit své heslo" + submit: Obnovit heslo + title: Zapomněli jste heslo? unlock: - title: "Zaslání instrukcí k odemčení účtu" - submit: "Zaslat instrukce k odemčení účtu" - links: - sign_in: "Přihlásit se" - sign_up: "Registrovat se" - forgot_your_password: "Zapomněli jste heslo?" - sign_in_with_omniauth_provider: "Přihlásit se přes %{provider}" - access_denied: - message: "Nemáte oprávnění k provedení této akce." + 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" - block: "Seznam" - grid: "Tabulka" - blog: "Blog" + 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 bc2f554e098..fab88913131 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -1,106 +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}" - edit_model: "Rediger %{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" - status_tag: - "yes": "Ja" - "no": "Nej" - 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" - multiple_without_total: "Viser %{model} %{from} - %{to}" - entry: - one: "post" - other: "poster" - any: "Alle" - blank_slate: - content: "Der er ingen %{resource_name} endnu." - link: "Opret" - dropdown_actions: - button_label: "Handlinger" + access_denied: + message: Du har ikke rettigheder til at udføre denne handling. + any: Alle batch_actions: - 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}?" - 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 d27b527e754..00000000000 --- a/config/locales/de-CH.yml +++ /dev/null @@ -1,102 +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" - edit_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" - status_tag: - "yes": "Ja" - "no": "Nicht" - 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}" - multiple_without_total: "Zeige %{model} %{from} – %{to}" - entry: - one: "Eintrag" - other: "Einträge" - any: "Alle" - blank_slate: - content: "Es gibt noch keine %{resource_name}." - link: "Erstellen" - dropdown_actions: - button_label: "Aktionen" - batch_actions: - button_label: "Stapelverarbeitung" - default_confirmation: "Bist du sicher, dass Sie dies tun wollen?" - delete_confirmation: "Sind Sie sicher dass sie diese %{plural_model} löschen wollen?" - 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" - delete: "Löschen" - delete_confirmation: "Sind Sie sicher dass sie diesen Kommentar löschen wollen?" - 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_up: "Registrieren" - sign_in: "Anmeldung" - forgot_your_password: "Passwort vergessen?" - sign_in_with_omniauth_provider: "Anmeldung mit %{provider}" - resend_unlock_instructions: "Entsperrungsanweisung erneut senden" - resend_confirmation_instructions: "Bestätigungsanweisung erneut senden" - unsupported_browser: - headline: "ActiveAdmin unterstützt nicht länger den Internet Explorer in Version 8 oder niedriger." - recommendation: "Wir empfehlen die Nutzung von Internet Explorer, Google Chrome, oder Firefox." - turn_off_compatibility_view: "Wenn sie IE 9 oder neuer benutzen, stellen sie sicher das sie den \"Kompatibilitätsansicht\" ausgeschaltet haben." diff --git a/config/locales/de.yml b/config/locales/de.yml index 446e0022733..91f37cf8ce7 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1,134 +1,143 @@ +--- de: active_admin: - 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" - edit_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ößer als" - less_than: "Kleiner als" - search_status: - headline: "Anwendungsbereich:" - current_filters: "Aktuelle Filter:" - no_current_filters: "Keine" - status_tag: - "yes": "Ja" - "no": "Nein" - main_content: "Bitte implementieren Sie %{model}#main_content, um Inhalte anzuzeigen." - logout: "Abmelden" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filter" - search_status: "Suchstatus" - pagination: - empty: "Keine %{model} gefunden" - one: "Zeige 1 %{model}" - one_page: "Zeige alle %{n} %{model}" - multiple: "Zeige %{model} %{from} – %{to} von %{total}" - multiple_without_total: "Zeige %{model} %{from} – %{to}" - entry: - one: "Eintrag" - other: "Einträge" - any: "Alle" - blank_slate: - content: "Es gibt noch keine %{resource_name}." - link: "Erstellen" - dropdown_actions: - button_label: "Aktionen" + access_denied: + message: Sie haben nicht die Berechtigung um diese Aktion auszuführen. + any: Alle batch_actions: - button_label: "Stapelverarbeitung" - default_confirmation: "Bist du sicher, dass Sie dies tun wollen?" - delete_confirmation: "Sind Sie sicher dass sie diese %{plural_model} löschen wollen?" - 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" + 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ösche" + 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: - resource_type: "Res­sour­cen-Typ" - author_type: "Autor-Typ" - body: "Inhalt" - author: "Autor" - title: "Kommentar" - add: "Kommentar hinzufügen" - delete: "Löschen" - delete_confirmation: "Sind Sie sicher dass sie diesen Kommentar löschen wollen?" - resource: "Res­sour­ce" - no_comments_yet: "Es gibt noch keine Kommentare." - author_missing: "Unbekannt" - title_content: "Kommentare (%{count})" + 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." + 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 + delete: Löschen + delete_confirmation: Wollen Sie dieses Element wirklich löschen? + delete_model: "%{model} löschen" + details: "%{model} Details" devise: - username: - title: "Benutzername" + change_password: + submit: Mein Passwort ändern + title: Ändern Sie Ihr Passwort email: - title: "E-Mail-Adresse" - subdomain: - title: "Subdomain" - password: - title: "Passwort" - sign_up: - title: "Registrieren" - submit: "Registrieren" + 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: - title: "Login" - remember_me: "Angemeldet bleiben" - submit: "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: - title: "Passwort vergessen?" - submit: "Mein Passwort zurücksetzen" - change_password: - title: "Ändern Sie Ihr Passwort" - submit: "Mein Passwort ändern" + submit: Mein Passwort zurücksetzen + title: Passwort vergessen? + sign_up: + submit: Registrieren + title: Registrieren + subdomain: + title: Subdomain unlock: - title: "Entsperrungsanweisung erneut senden" - submit: "Entsperrungsanweisung erneut senden" - resend_confirmation_instructions: - title: "Anleitung zur Bestätigung noch mal schicken" - submit: "Anleitung zur Bestätigung noch mal schicken" - links: - sign_up: "Registrieren" - sign_in: "Anmeldung" - forgot_your_password: "Passwort vergessen?" - sign_in_with_omniauth_provider: "Anmeldung mit %{provider}" - resend_unlock_instructions: "Entsperrungsanweisung erneut senden" - resend_confirmation_instructions: "Bestätigungsanweisung erneut senden" - unsupported_browser: - headline: "ActiveAdmin unterstützt nicht länger den Internet Explorer in Version 8 oder niedriger." - recommendation: "Wir empfehlen die Nutzung von Internet Explorer, Google Chrome, oder Firefox." - turn_off_compatibility_view: "Wenn sie IE 9 oder neuer benutzen, stellen sie sicher das sie den \"Kompatibilitätsansicht\" ausgeschaltet haben." - access_denied: - message: "Sie haben nicht die Berechtigung um diese Aktion auszuführen." + submit: Entsperrungsanweisung erneut senden + title: Entsperrungsanweisung erneut senden + username: + title: Benutzername + download: 'Herunterladen:' + edit: Bearbeiten + edit_model: "%{model} bearbeiten" + empty: Leer + filters: + buttons: + clear: Filter entfernen + filter: Filtern + predicates: + from: Von + to: Bis + has_many_delete: Löschen + has_many_new: "%{model} hinzufügen" + has_many_remove: Entfernen index_list: - table: "Tabelle" - block: "Liste" - grid: "Gitter" - blog: "Blog" + table: Tabelle + logout: Abmelden + move: Verschieben + new_model: "%{model} erstellen" + next: Weiter + pagination: + empty: Keine %{model} gefunden + entry: + 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: + active_admin/comment: + author_type: Autortyp + body: Inhalt + created_at: Erstellt + namespace: Namensraum + resource_type: Ressourcentyp + updated_at: Aktualisiert + models: + active_admin/comment: + one: Kommentar + other: Kommentare + comment: + one: Kommentar + other: Kommentare diff --git a/config/locales/el.yml b/config/locales/el.yml index 96444967839..29eaa60a4c3 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -1,127 +1,107 @@ +--- el: 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: "Μικρότερο από" - status_tag: - "yes": "Ναι" - "no": "Δεν" - 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} συνολικά" - multiple_without_total: "Εμφανίζονται %{model} %{from} - %{to}" - entry: - one: "εγγραφή" - other: "εγγραφές" - any: "Όλες οι εγγραφές" - blank_slate: - content: "Δεν υπάρχουν %{resource_name} ακόμα." - link: "Δημιουργήστε μία εγγραφή" - dropdown_actions: - button_label: "Ενέργειες" + access_denied: + message: Δεν έχετε πρόσβαση για αυτή την ενέργεια. + any: Όλες οι εγγραφές batch_actions: - button_label: "Μαζικές Ενέργειες" - default_confirmation: "Είστε σίγουρος πως θέλετε να το κάνετε αυτό;" - 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: "Δεν υπάρχει κανένα σχόλιο." - author_missing: "Ανώνυμος" - 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: - username: - title: "Όνομα χρήστη" + change_password: + submit: Αλλαγή του κωδικού + title: Αλλάξτε τον κωδικό σας email: - title: "Email" - subdomain: - title: "Subdomain" - password: - title: "Κωδικός" - sign_up: - title: "Εγγραφή" - submit: "Εγγραφή" + title: Email + 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: Subdomain unlock: - title: "Αποστολή οδηγιών ξεκλειδώματος" - submit: "Αποστολή οδηγιών ξεκλειδώματος" - resend_confirmation_instructions: - title: "Αποστολή οδηγιών επιβεβαίωσης" - submit: "Αποστολή οδηγιών επιβεβαίωσης" - links: - sign_in: "Σύνδεση" - sign_up: "Εγγραφή" - forgot_your_password: "Ξεχάσατε τον κωδικό σας;" - sign_in_with_omniauth_provider: "Σύνδεση με %{provider}" - resend_unlock_instructions: "Αποστολή οδηγιών ξεκλειδώματος" - resend_confirmation_instructions: "Αποστολή οδηγιών επιβεβαίωσης" - unsupported_browser: - headline: "Το ActiveAdmin δεν υποστηρίζει πλεον τον Internet Explorer έκδοση 8 η μικρότερη." - recommendation: "Σας προτείνουμε να αναβαθμίσετε στην τελευταία Internet Explorer, Google Chrome, or Firefox." - turn_off_compatibility_view: "Αν χρησιμοποιείτε IE 9 ή μεγαλύτερη έκδοση, σιγουρευτείτε ότι turn off \"Compatibility View\"." - 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: "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: 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 fd06bba52fe..1650178ffdc 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -1,98 +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}" - edit_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" - status_tag: - "yes": "Yes" - "no": "No" - 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" - multiple_without_total: "Displaying %{model} %{from} - %{to}" - entry: - one: "entry" - other: "entries" - any: "Any" - blank_slate: - content: "There are no %{resource_name} yet." - link: "Create one" - dropdown_actions: - button_label: "Actions" + access_denied: + message: You are not authorised to perform this action. + any: Any batch_actions: - 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}?" - 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." - author_missing: "Anonymous" - 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" - resend_confirmation_instructions: - title: "Resend confirmation instructions" - submit: "Resend confirmation instructions" + 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}" - resend_unlock_instructions: "Re-send unlock instructions" + 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 b8614bfd73a..96da53870ce 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,135 +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}" - edit_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" - search_status: - headline: "Scope:" - current_filters: "Current filters:" - no_current_filters: "None" - status_tag: - "yes": "Yes" - "no": "No" - main_content: "Please implement %{model}#main_content to display content." - logout: "Logout" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filters" - search_status: "Search Status" - pagination: - empty: "No %{model} found" - one: "Displaying 1 %{model}" - one_page: "Displaying all %{n} %{model}" - multiple: "Displaying %{model} %{from} - %{to} of %{total} in total" - multiple_without_total: "Displaying %{model} %{from} - %{to}" - entry: - one: "entry" - other: "entries" - any: "Any" - blank_slate: - content: "There are no %{resource_name} yet." - link: "Create one" - dropdown_actions: - button_label: "Actions" + access_denied: + message: You are not authorized to perform this action. + any: Any batch_actions: - 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}?" - 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: - created_at: "Created" - resource_type: "Resource Type" - author_type: "Author Type" - body: "Body" - author: "Author" - title: "Comment" - add: "Add Comment" - delete: "Delete Comment" - delete_confirmation: "Are you sure you want to delete these comment?" - resource: "Resource" - no_comments_yet: "No comments yet." - author_missing: "Anonymous" - 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: - username: - title: "Username" + change_password: + submit: Change my password + title: Change your password email: - title: "Email" - subdomain: - title: "Subdomain" - password: - title: "Password" - sign_up: - title: "Sign up" - submit: "Sign up" + 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" - resend_confirmation_instructions: - title: "Resend confirmation instructions" - submit: "Resend confirmation instructions" - links: - sign_up: "Sign up" - sign_in: "Sign in" - forgot_your_password: "Forgot your password?" - sign_in_with_omniauth_provider: "Sign in with %{provider}" - resend_unlock_instructions: "Resend unlock instructions" - resend_confirmation_instructions: "Resend confirmation instructions" - unsupported_browser: - headline: "Please note that ActiveAdmin no longer supports Internet Explorer versions 8 or less." - recommendation: "We recommend upgrading to the latest Internet Explorer, Google Chrome, or Firefox." - turn_off_compatibility_view: "If you are using IE 9 or later, make sure you turn off \"Compatibility View\"." - 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 index f034caba4f3..9cf3d0acbd6 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -1,94 +1,82 @@ +--- 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}" - edit_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: "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" - status_tag: - "yes": "Sí" - "no": "No" - 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" - any: "Cualquiera" - dropdown_actions: - button_label: "Acciones" + any: Cualquiera batch_actions: - button_label: "Acciones en masa" + action_label: "%{title} seleccionados" + button_label: Acciones en masa default_confirmation: "¿Seguro que quieres hacer esto?" - delete_confirmation: "Eliminar %{plural_model}: ¿Está seguro?" - 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" + delete_confirmation: 'Eliminar %{plural_model}: ¿Está seguro?' 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 + body: Cuerpo 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: 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: - 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 links: - sign_in: "registrarse" forgot_your_password: "¿Olvidó su contraseña?" - sign_in_with_omniauth_provider: "Conéctate con %{provider}" + 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" - block: "Lista" - grid: "Cuadrícula" - blog: "Blog" + 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 c8f54d0f975..4bdb605c202 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1,135 +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}" - edit_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: "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" - search_status: - headline: "Alcance:" - current_filters: "Filtros actuales:" - no_current_filters: "Ninguno" - status_tag: - "yes": "Sí" - "no": "No" - main_content: "Por favor implemente %{model}#main_content para mostrar contenido." - logout: "Salir" - powered_by: "Funciona con %{active_admin} %{version}" - sidebars: - filters: "Filtros" - search_status: "Estado de la búsqueda" - 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}" - multiple_without_total: "Mostrando %{model} %{from} - %{to}" - entry: - one: "registro" - other: "registros" - any: "Cualquiera" - blank_slate: - content: "No hay %{resource_name} aún." - link: "Añadir" - dropdown_actions: - button_label: "Acciones" + access_denied: + message: No está autorizado/a a realizar esta acción. + any: Cualquiera batch_actions: - button_label: "Acciones en masa" + action_label: "%{title} seleccionados" + button_label: Acciones en masa default_confirmation: "¿Seguro que quieres hacer esto?" - delete_confirmation: "Eliminar %{plural_model}: ¿Está seguro?" - 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" + 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: - created_at: "Fecha de creación" - resource_type: "Tipo de recurso" - author_type: "Tipo de autor" - body: "Cuerpo" - author: "Autor" - title: "Comentario" - add: "Comentar" - delete: "Borrar Comentario" - delete_confirmation: "¿Está seguro que desea borrar este comentario?" - resource: "Recurso" - no_comments_yet: "No hay comentarios aún." - author_missing: "Anónimo" - 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: - username: - title: "Nombre de usuario" + change_password: + submit: Cambiar mi contraseña + title: Cambie su contraseña email: - title: "Email" - subdomain: - title: "Subdominio" - password: - title: "Password" - sign_up: - title: "Registrarse" - submit: "Registrarse" + title: Email + links: + forgot_your_password: "¿Olvidó su contraseña?" + 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: - title: "iniciar sesión" - remember_me: "Recordarme" - submit: "iniciar sesión" + 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?" - submit: "Restablecer mi contraseña" - change_password: - title: "Cambie su contraseña" - submit: "Cambiar mi contraseña" + sign_up: + submit: Registrarse + title: Registrarse + subdomain: + title: Subdominio unlock: - title: "Reenviar instrucciones de desbloqueo" - submit: "Reenviar instrucciones de desbloqueo" - resend_confirmation_instructions: - title: "Reenviar instrucciones de confirmación" - submit: "Reenviar instrucciones de confirmación" - links: - sign_up: "Ingresar" - sign_in: "registrarse" - forgot_your_password: "¿Olvidó su contraseña?" - sign_in_with_omniauth_provider: "Conéctate con %{provider}" - resend_unlock_instructions: "Reenviar instrucciones de desbloqueo" - resend_confirmation_instructions: "Reenviar instrucciones de confirmación" - unsupported_browser: - headline: "Por favor tenga en cuenta que Active Admin no soporta versiones de Internet Explorer menores a 8." - recommendation: "Recomendamos que actualice a la última versión de Internet Explorer, Google Chrome, o Firefox." - turn_off_compatibility_view: "Si está usando IE 9 o superior, asegúrese de apagar la \"Vista de compatibilidad\"." - access_denied: - message: "No está autorizado/a a realizar esta acción." + 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" - block: "Lista" - grid: "Grilla" - blog: "Blog" + 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/fa.yml b/config/locales/fa.yml index 1cebc612890..cbbf6cbec30 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -1,120 +1,104 @@ +--- fa: active_admin: - dashboard: "داشبرد" - dashboard_welcome: - welcome: "به اکتیو ادمین خوش آمدید. این صفحه اول داشبرد است." - 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: "کوچکتر از" - status_tag: - "yes": "بله" - "no": "بدون" - 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} رکورد" - multiple_without_total: "نمایش %{model} %{from} - %{to}" - entry: - one: "آیتم" - other: "آیتم‌ها" - any: "هرکدام" - blank_slate: - content: "هنوز هیچ رکوردی از %{resource_name} درج نشده." - link: "درج اولین رکورد" - dropdown_actions: - button_label: "عملیات" + access_denied: + message: شما دسترسی لازم برای انجام این عملیات را ندارید. + any: هرکدام batch_actions: - button_label: "عملیات‌های دسته‌ای" - default_confirmation: "آیا برای اجرای این عملیات اطمینان دارید؟" - 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: "هنوز هیچ کامنتی نوشته نشده." - author_missing: "بی‌نام" - 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: - username: - title: "نام کاربری" + change_password: + submit: تغییر کلمه عبور + title: تغییر کلمه عبور email: - title: "ایمیل" - subdomain: - title: "Subdomain" - password: - title: "کلمه‌عبور" - sign_up: - title: "ثبت‌نام" - submit: "ثبت‌نام" + 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: Subdomain unlock: - title: "ارسال مجدد دستورالعمل بازگشایی حساب کاربری" - submit: "ارسال مجدد دستورالعمل بازگشایی حساب کاربری" - resend_confirmation_instructions: - 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/fi.yml b/config/locales/fi.yml index fb0ba81b203..0daaeef830f 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -1,113 +1,97 @@ +--- fi: active_admin: - dashboard: Etusivu - dashboard_welcome: - welcome: "Tervetuloa! Tämä on Active Adminin oletusetusivu." - call_to_action: "Lisätäksesi etusivun osioita katso: 'app/admin/dashboard.rb'" - view: "Katso" - edit: "Muokkaa" - delete: "Poista" - delete_confirmation: "Oletko varma, että haluat poistaa tämän?" - new_model: "Uusi %{model}" - edit_model: "Muokaa %{model}" - delete_model: "Poista %{model}" - details: "%{model} Tiedot" - cancel: "Peruuta" - empty: "Tyhjä" - previous: "Edellinen" - next: "Seuraava" - download: "Lataa:" - has_many_new: "Lisää uusi %{model}" - has_many_delete: "Poista" - has_many_remove: "Poista" - filters: - buttons: - filter: "Hae" - clear: "Tyhjennä valinnat" - predicates: - contains: "Sisältää" - equals: "On yhtä kuin" - starts_with: "Alkaa" - ends_with: "Päättyy" - greater_than: "Suurempi kuin" - less_than: "Pienempi kuin" - status_tag: - "yes": "Kyllä" - "no": "Ei" - main_content: "Ole hyvä, käytä %{model}#main_content:ia nähdäksesi jotain." - logout: "Kirjaudu ulos" - powered_by: "Käyttää %{active_admin} %{version}:ia" - sidebars: - filters: "Haku" - pagination: - empty: "%{model}:ia ei löytynyt" - one: "Näytetään 1 %{model}" - one_page: "Näytetään kaikki %{n} %{model}:it" - multiple: "Näytetään %{model} %{from} - %{to} (yhteensä %{total})" - multiple_without_total: "Näytetään %{model} %{from} - %{to}" - entry: - one: "syöte" - other: "syötteet" - any: "mikä vain" - blank_slate: - content: "Järjestelmässä ei ole yhtään %{resource_name}:ia vielä." - link: "Luo ensimmäinen" - dropdown_actions: - button_label: "Acciones" + access_denied: + message: Sinulla ei ole oikeuksia suorittaa yrittämääsi toimintoa. + any: mikä vain batch_actions: - 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?" - succesfully_destroyed: - one: "1 %{model} poistettu" - other: "%{count} %{plural_model}:a poistettu" - selection_toggle_explanation: "(Vaihda valintaa)" - link: "Luo" 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" + 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: - resource_type: "Resurssityyppi" - author_type: "Luoja-tyyppi" - body: "Runko" - author: "Luoja" - title: "Kommentti" - add: "Lisää kommentti" - resource: "Resurssi" - no_comments_yet: "Ei kommentteja." - title_content: "Kommentteja (%{count})" + add: Lisää kommentti + author: Luoja + author_type: Luoja-tyyppi + body: Runko errors: - empty_text: "Kommenttia ei pystytty tallentamaan, et kirjoittanut kommenttitekstiä." + 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: - username: - title: "Käyttäjänimi" + change_password: + submit: Vaihda salasana + title: Vaihda salasana email: - title: "Sähköposti" - subdomain: - title: "Subdomain" - password: - title: "Salasana" + 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: - title: "Sisäänkirjautuminen" - remember_me: "Muista minut" - submit: "Kirjaudu sisään" + remember_me: Muista minut + submit: Kirjaudu sisään + title: Sisäänkirjautuminen + password: + title: Salasana reset_password: - title: "Unohtunut salasana?" - submit: "Resetoi salasana" - change_password: - title: "Vaihda salasana" - submit: "Vaihda salasana" + submit: Resetoi salasana + title: Unohtunut salasana? + subdomain: + title: Subdomain unlock: - title: "Lähetä ohjeet lukituksen poistoon" - submit: "Lähetä ohjeet lukituksen poistoon" - links: - sign_in: "Kirjaudu sisään" - forgot_your_password: "Unohtunut salasana?" - sign_in_with_omniauth_provider: "Kirjaudu sisään %{provider}:ia käyttäen" - access_denied: - message: "Sinulla ei ole oikeuksia suorittaa yrittämääsi toimintoa." + 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" - block: "Lista" - grid: "Ruudukko" - blog: "Blogi" + 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 91e985ac745..0a1feb204ec 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1,118 +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: "Créer %{model}" - edit_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: "Égal à" - starts_with: "Commence par" - ends_with: "Se termine par" - greater_than: "Plus grand que" - less_than: "Plus petit que" - search_status: - headline: "Etendu du filtre :" - current_filters: "Filtres actuels :" - no_current_filters: "Aucun filtres" - status_tag: - "yes": "Oui" - "no": "Non" - 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" - search_status: "Statut de la recherche" - pagination: - empty: "Aucun %{model} trouvé" - one: "Affichage de 1 %{model}" - one_page: "Affichage des %{n} %{model}" - multiple: "Affichage de %{model} %{from} - %{to} sur un total de %{total}" - multiple_without_total: "Affichage de %{model} %{from} - %{to}" - 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" - dropdown_actions: - button_label: "Actions" + access_denied: + message: Vous n'êtes pas autorisé à exécuter cette action + any: N'importe lequel batch_actions: - button_label: "Actions groupées" - default_confirmation: "Êtes-vous sûr de vouloir faire cela ?" - delete_confirmation: "Êtes-vous sûr de vouloir supprimer ces %{plural_model} ?" - 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" - resource_type: "Type de ressource" - author_type: "Profil de l'auteur" - 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: - username: - title: "Nom d'utilisateur" + change_password: + submit: Changer mon mot de passe + title: Changez votre mot de passe email: - title: "Email" - subdomain: - title: "Sous-domaine" - password: - title: "Mot de passe" + title: Email + links: + 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: - title: "Connexion" - remember_me: "Garder ma session ouverte" - submit: "Se connecter" - 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" + 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: - title: "Renvoyer les instructions de confirmation" - submit: "Renvoyer les instructions de confirmation" - links: - sign_in: "Connectez-vous" - forgot_your_password: "Vous avez oublié votre mot de passe ?" - sign_in_with_omniauth_provider: "Connectez-vous avec %{provider}" - access_denied: - message: "Vous n'êtes pas autorisé à exécuter cette action" + 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" - block: "Liste" - grid: "Grille" - blog: "Blog" + 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 0c3f28aaa5e..fbbca6d504b 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -1,95 +1,117 @@ +--- he: active_admin: - dashboard: פנל ניהול - dashboard_welcome: - welcome: "ברוכים הבאים לאקטיב אדמין. זהו פנל הניהול" - 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: "פחות מ" - status_tag: - "yes": "כן" - "no": "לא" - 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} בסך הכל" - multiple_without_total: "מציג %{model} %{from} - %{to}" - entry: - one: "רשומה בודדה" - other: "רשומות" - any: "Any" - blank_slate: - content: "כרגע אין עוד אף %{resource_name}." - link: "צור אחד" - dropdown_actions: - button_label: "פעולו" + access_denied: + message: אינך רשאי לבצע פעולה זו. + any: Any batch_actions: - button_label: "פעולות מרובות" - default_confirmation: "אתה בטוח שאתה רוצה לעשות את זה?" - 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 651a13d8f45..11756d35244 100644 --- a/config/locales/hr.yml +++ b/config/locales/hr.yml @@ -1,124 +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 biste 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}" - edit_model: "Uredi %{model}" - delete_model: "Obriši %{model}" - details: "%{model} detalji" - cancel: "Odustani" - empty: "Prazno" - previous: "Prijašnji" - next: "Sljedeć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" - status_tag: - "yes": "Da" - "no": "Nema" - main_content: "Molim Vas, implementirajte %{model}#main_content da biste 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}" - multiple_without_total: "Prikazani %{model} %{from} - %{to}" - 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" - dropdown_actions: - button_label: "Ukrepi" + access_denied: + message: Nemaš dopuštenja. + any: Bilo koji batch_actions: - button_label: "Grupne akcije" - default_confirmation: "Jeste li sigurni da želite to učiniti?" - delete_confirmation: "Jeste li sigurni da želite obrisati %{plural_model}?" - 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: "(Izmijeni 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: - resource_type: "Tip objekta" - author_type: "Tip autora" - body: "Sadržaj" - author: "Autor" - title: "Komentar" - add: "Dodaj komentar" - resource: "Objekt" - no_comments_yet: "Još nema komentara." - author_missing: "Anoniman" - 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: - username: - title: "Korisničko ime" + change_password: + submit: Izmijeni lozinku + title: Izmjena lozinke email: - title: "Email" - subdomain: - title: "Poddomena" - password: - title: "Lozinka" - sign_up: - title: "Registracija" - submit: "Registruj" + title: Email + links: + forgot_your_password: Zaboravljena lozinka? + sign_in: Prijavi se + sign_in_with_omniauth_provider: Prijavite se za %{provider} login: - title: "Prijava" - remember_me: "Zapamti me" - submit: "Prijavi se" + 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: - title: "Zaboravljena lozinka?" - submit: "Resetiraj lozinku" - change_password: - title: "Izmjena lozinke" - submit: "Izmijeni lozinku" + submit: Resetiraj lozinku + title: Zaboravljena lozinka? + sign_up: + submit: Registruj + title: Registracija + subdomain: + title: Poddomena unlock: - title: "Ponovno slanje uputstva za otključavanje" - submit: "Pošalji" - resend_confirmation_instructions: - title: "Ponovno slanje uputstva za potvrdu" - submit: "Pošalji" - links: - sign_in: "Prijavi se" - forgot_your_password: "Zaboravljena lozinka?" - sign_in_with_omniauth_provider: "Prijavite se za %{provider}" - access_denied: - message: "Nemaš dopuštenja." + 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" - block: "Lista" - grid: "Rešetka" - blog: "Blog" + 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 844f0315539..aac702853af 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -1,99 +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}" - edit_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" - status_tag: - "yes": "Igen" - "no": "Nem" - 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} " - multiple_without_total: "%{model} listájának megjelenítése, %{from} - %{to} " - entry: - one: "elem" - other: "elem" - any: "Összes" - blank_slate: - content: "Még nincs létrehozva %{resource_name}." - link: "Létrehozás most" - dropdown_actions: - button_label: "Műveletek" + any: Összes batch_actions: - 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}?" - 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" - resend_confirmation_instructions: - title: "Megerősítő levél újraküldése" - submit: "Megerősítő levél újraküldése" - 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 index 8bc179eeb14..862a32b2e01 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -1,135 +1,113 @@ +--- id: active_admin: - dashboard: Dashboard - dashboard_welcome: - welcome: "Selamat datang di Active Admin. Ini adalah tampilan standar halaman dashboard." - call_to_action: "Tampilan halaman ini bisa diubah di file 'app/admin/dashboard.rb'" - view: "Lihat" - edit: "Ubah" - delete: "Hapus" - delete_confirmation: "Apakah anda yakin ingin menghapus data ini?" - new_model: "Tambah %{model} baru" - edit_model: "Ubah %{model}" - delete_model: "Hapus %{model}" - details: "Detail %{model}" - cancel: "Batal" - empty: "Kosong" - previous: "Sebelumnya" - next: "Berikutnya" - download: "Unduh:" - has_many_new: "Tambah %{model} baru" - has_many_delete: "Hapus" - has_many_remove: "Hapus" - filters: - buttons: - filter: "Filter" - clear: "Hapus Filters" - predicates: - contains: "Mengandung" - equals: "Sama dengan" - starts_with: "Diawali dengan" - ends_with: "Diakhiri dengan" - greater_than: "Lebih besar dari" - less_than: "Lebih kecil dari" - search_status: - headline: "Scope:" - current_filters: "Filter kini:" - no_current_filters: "Tidak ada" - status_tag: - "yes": "Ya" - "no": "Tidak" - main_content: "Harap mengimplementasikan %{model}#main_content untuk menampilkan konten." - logout: "Keluar" - powered_by: "Dibuat dengan %{active_admin} %{version}" - sidebars: - filters: "Filter" - search_status: "Status Pencarian" - pagination: - empty: "Tidak ada %{model} yang bisa ditemukan" - one: "Menampilkan 1 %{model}" - one_page: "Menampilkan semua %{n} %{model}" - multiple: "Menampilkan %{from} - %{to} dari %{total} keseluruhan %{model}" - multiple_without_total: "Menampilkan %{from} - %{to} %{model}" - entry: - one: "data" - other: "data" - any: "Apapun" - blank_slate: - content: "%{resource_name} masih belum ada sama sekali." - link: "Tambah data" - dropdown_actions: - button_label: "Tindakan" + access_denied: + message: Anda tidak diperkenankan melakukan aksi tersebut. + any: Apapun batch_actions: - button_label: "Tindakan Serentak" - default_confirmation: "Apakah anda yakin akan melakukan ini?" - delete_confirmation: "Apakah anda yakin akan menghapus %{plural_model}?" - succesfully_destroyed: - one: "Berhasil menghapus %{model}" - other: "Berhasil menghapus %{count} %{plural_model}" - selection_toggle_explanation: "(Tampilkan Pilihan)" - link: "Tambah data" 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" + 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: - created_at: "Dibuat" - resource_type: "Jenis Resource" - author_type: "Tipe Penulis" - body: "Isi" - author: "Penulis" - title: "Komentar" - add: "Tambah Komentar" - delete: "Hapus Komentar" - delete_confirmation: "Apakah anda yakin akan menghapus komentar tersebut?" - resource: "Resource" - no_comments_yet: "Belum ada komentar sama sekali." - author_missing: "Anonim" - title_content: "Komentar (%{count})" + 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." + 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: - username: - title: "Username" + change_password: + submit: Kirimkan instruksi pengaturan ulang password + title: " - Atur Ulang Password" email: - title: "Email" - subdomain: - title: "Subdomain" - password: - title: "Password" - sign_up: - title: " - Daftar" - submit: "Daftar" + 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" - remember_me: "Ingat saya" - submit: "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" - submit: "Atur ulang password" - change_password: - title: " - Atur Ulang Password" - submit: "Kirimkan instruksi pengaturan ulang password" + sign_up: + submit: Daftar + title: " - Daftar" + subdomain: + title: Subdomain unlock: + submit: Kirimkan instruksi pengaktifan kembali akun title: " - Kirim Instruksi Pengaktifan Kembali Akun" - submit: "Kirimkan instruksi pengaktifan kembali akun" - resend_confirmation_instructions: - title: " - Kirim Lagi Instruksi Konfirmasi Akun" - submit: "Kirimkan lagi instruksi konfirmasi akun" - links: - sign_up: "Daftar" - sign_in: "Masuk" - forgot_your_password: "Lupa password?" - sign_in_with_omniauth_provider: "Daftar melalui %{provider}" - resend_unlock_instructions: "Kirim instruksi pengaktifan kembali akun" - resend_confirmation_instructions: "Kirim lagi instruksi konfirmasi akun" - unsupported_browser: - headline: "Harap dicatat bahwa ActiveAdmin sudah tidak mendukung InternetExplorer versi 8 atau versi sebelum itu." - recommendation: "Kami sarankan agar anda mengupgrade ke versi Internet Explorer, Google Chrome, atau Firefox yang terbaru." - turn_off_compatibility_view: "Kalau anda menggunakan IE 9 atau yang lebih baru, pastikan anda mematikan \"Compatibility View\"." - access_denied: - message: "Anda tidak diperkenankan melakukan aksi tersebut." + 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" - block: "Daftar" - grid: "Grid" - blog: "Blog" + 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 15b28ff70be..10d575f2516 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1,135 +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}" - edit_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" - search_status: - headline: "Contesto:" - current_filters: "Filtri attivi:" - no_current_filters: "Nessuno" - status_tag: - "yes": "Sì" - "no": "No" - main_content: "Devi implemetare %{model}#main_content per mostrarne il contenuto." - logout: "Esci" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtri" - search_status: "Informazioni sulla ricerca" - 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" - multiple_without_total: "Sto mostrando %{model} %{from} - %{to}" - entry: - one: "voce" - other: "voci" - any: "Qualsiasi" - blank_slate: - content: "Non sono presenti %{resource_name}" - link: "Crea nuovo/a" - dropdown_actions: - button_label: "Azioni" + access_denied: + message: Non hai le autorizzazioni necessarie per eseguire questa azione. + any: Qualsiasi batch_actions: - button_label: "Azioni multiple" - default_confirmation: "Sei sicuro di che voler fare questo?" - delete_confirmation: "Sei sicuro di volere cancellare %{plural_model}?" - 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: - created_at: "Creato il" - resource_type: "Tipo di risorsa" - author_type: "Tipo di Autore" - body: "Corpo" - author: "Autore" - title: "Commento" - add: "Aggiungi Commento" - delete: "Cancella Commento" - delete_confirmation: "Sei sicuro di voler cancellare questo commento?" - resource: "Risorsa" - no_comments_yet: "Nessun commento." - author_missing: "Anonimo" - 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: - username: - title: "Nome Utente" + change_password: + submit: Cambia la mia password + title: Cambia la tua password email: - title: "Email" - subdomain: - title: "Sottodominio" - password: - title: "Password" - sign_up: - title: "Iscriviti" - submit: "Iscriviti" + title: Email + links: + 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: - title: "Entra" - remember_me: "Ricordami" - submit: "Entra" + 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: - title: "Dimenticato la password?" - submit: "Reimposta la tua password" - change_password: - title: "Cambia la tua password" - submit: "Cambia la mia password" + submit: Reimposta la tua password + title: Dimenticato la password? + sign_up: + submit: Iscriviti + title: Iscriviti + subdomain: + title: Sottodominio unlock: - title: "Invia di nuovo le istruzioni per sbloccare" - submit: "Invia di nuovo le istruzioni per sbloccare" - resend_confirmation_instructions: - title: "Invia di nuovo le istruzioni per la conferma" - submit: "Invia di nuovo le istruzioni per la conferma" - links: - sign_in: "Iscriviti" - sign_in: "Entra" - forgot_your_password: "Dimenticato la password?" - sign_in_with_omniauth_provider: "Collegati a %{provider}" - resend_unlock_instructions: "Invia di nuovo le istruzioni per lo sblocco" - resend_confirmation_instructions: "Invia di nuovo le istruzioni per la conferma" - unsupported_browser: - headline: "Perfavore, notare che ActiveAdmin non supporta più Internet Explorer 8 o inferiore" - recommendation: "Ti raccomandiamo di aggiornare alla versione più recente di Internet Explorer, Google Chrome, o Firefox." - turn_off_compatibility_view: "Se stai utilizzando Internet Explorer 9 o successivo, assicurati di disabilitare la \"Modalità Compatibilità\"." - access_denied: - message: "Non hai le autorizzazioni necessarie per eseguire questa azione." + 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" - block: "Lista" - grid: "Griglia" - blog: "Blog" + 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 91ddbb4cc02..54cfb078fde 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1,135 +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} を作成する" - 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: "より小さい" - search_status: - headline: "範囲:" - current_filters: "現在の絞り込み:" - no_current_filters: "なし" - status_tag: - "yes": "はい" - "no": "いいえ" - main_content: "内容を表示するために %{model}#main_content を実装してください。" - logout: "ログアウト" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "検索条件" - search_status: "検索状態" - pagination: - empty: "%{model} は見つかりませんでした" - one: "1 件の %{model} を表示しています" - one_page: "全 %{n} 件の %{model} を表示しています" - multiple: "全 %{total} 件中 %{from} - %{to} 件の %{model} を表示しています" - multiple_without_total: "%{from} - %{to} 件の %{model} を表示しています" - entry: - one: "レコード" - other: "レコード" - any: "任意" - blank_slate: - content: "%{resource_name} はまだありません。" - link: "作成する" - dropdown_actions: - button_label: "操作" + access_denied: + message: アクションを実行する権限がありません + any: 任意 batch_actions: - button_label: "一括操作" - default_confirmation: "本当によろしいですか?" + action_label: 選択した行を%{title} + button_label: 一括操作 + default_confirmation: 本当によろしいですか? delete_confirmation: "%{plural_model} を削除してもよろしいですか?" - succesfully_destroyed: - one: "1件の %{model} を削除しました" - other: "%{count}件の %{plural_model} を削除しました" - selection_toggle_explanation: "(選択)" - link: "作成する" - action_label: "選択した行を%{title}" labels: - destroy: "削除する" + destroy: 削除する + selection_toggle_explanation: "(選択)" + successfully_destroyed: + one: 1件の %{model} を削除しました + other: "%{count}件の %{plural_model} を削除しました" + blank_slate: + content: "%{resource_name} はまだありません。" + link: 作成する + cancel: 取り消す comments: - created_at: "作成日" - resource_type: "リソース種別" - author_type: "作成者種別" - body: "本文" - author: "作成者" - title: "コメント" - add: "コメントを追加" - delete: "コメントを削除" - delete_confirmation: "本当にコメントを削除しますか?" - resource: "リソース" - no_comments_yet: "コメントはまだありません。" - author_missing: "匿名ユーザ" - 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: - username: - title: "ユーザ名" + change_password: + submit: パスワードを変更する + title: パスワードを変更する email: - title: "メールアドレス" - subdomain: - title: "サブドメイン" - password: - title: "パスワード" - sign_up: - title: "登録" - submit: "登録" + 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: "ロックの解除方法を送る" - resend_confirmation_instructions: - title: "確認方法を再送信する" - submit: "確認方法を再送信する" - links: - sign_in: "サインイン" - sign_up: "ユーザ登録" - forgot_your_password: "パスワードをお忘れですか?" - sign_in_with_omniauth_provider: "%{provider}のアカウントを使ってログイン" - resend_confirmation_instructions: "ユーザ確認手順を再送する" - resend_unlock_instructions: "ロックの解除方法を再送する" - unsupported_browser: - headline: "ActiveAdminは、Internet Explorer 8以下はサポートはしていません。" - recommendation: "最新版のInternet ExplorerGoogle Chrome、もしくはFirefoxを使うことを推奨します。" - turn_off_compatibility_view: "Internet Explorer 9以降を使っている場合、互換表示をオフにしてください。" - 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: ログアウト + 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} を表示しています" + 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 1d54fa6b4c3..1847883fa66 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -1,120 +1,148 @@ +--- ko: active_admin: - dashboard: "대시보드" - dashboard_welcome: - welcome: "ActiveAdmin에 오신 것을 환영합니다. 기본 대시보드 페이지 입니다." - 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: "미만" - search_status: - headline: "검색 범위:" - current_filters: "적용된 필터:" - no_current_filters: "현재 적용된 필터가 없습니다" - status_tag: - "yes": "있음" - "no": "없음" - main_content: "내용을 보시려면 %{model}#main_content의 코드를 먼저 구현해 주시기 바랍니다." - logout: "로그아웃" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "필터 목록" - search_status: "검색 상태" - pagination: - empty: "%{model} 이/가 없습니다." - one: "1개 %{model} 표시중" - one_page: "%{n}개 %{model} 표시중" - multiple: "%{total}개 중 %{from} - %{to} %{model} 표시중" - multiple_without_total: "%{from} - %{to} %{model} 표시중" - entry: - one: "항목" - other: "항목들" - any: "어떤" - blank_slate: - content: "아직 %{resource_name} 이/가 없습니다." - link: "추가하기" - dropdown_actions: - button_label: "작업" + access_denied: + message: 이 작업을 수행할 권한이 없습니다. + any: 어떤 batch_actions: - button_label: "배치 작업" - default_confirmation: "확실하십니까?" + action_label: 선택한 항목 %{title} + button_label: 배치 작업 + default_confirmation: 확실하십니까? delete_confirmation: "%{plural_model}을/를 삭제하시겠습니까?" - succesfully_destroyed: - one: "성공적으로 1개 %{model}을/를 삭제하였습니다" - other: "성공적으로 %{count}개의 %{plural_model}을/를 삭제하였습니다" - selection_toggle_explanation: "(선택 항목 바꾸기)" - link: "만들기" - action_label: "%{title} 선택됨" labels: - destroy: "삭제" + destroy: 삭제 + selection_toggle_explanation: "(선택 항목 바꾸기)" + successfully_destroyed: + one: 성공적으로 1개 %{model}을/를 삭제하였습니다 + other: 성공적으로 %{count}개의 %{plural_model}을/를 삭제하였습니다 + blank_slate: + content: 아직 %{resource_name} 이/가 없습니다. + link: 추가하기 + cancel: 취소 comments: - created_at: "작성시간" - 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: - username: - title: "아이디" + change_password: + submit: 내 비밀번호 변경 + title: 비밀번호 변경 email: - title: "이메일" - subdomain: - title: "서브도메인" - password: - title: "비밀번호" - sign_up: - title: "가입하기" - submit: "가입하기" + 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: "계정 잠금 해제하기" - resend_confirmation_instructions: - title: "계정 승인 요청하기" - submit: "계정 승인 요청하기" - links: - sign_in: "로그인" - forgot_your_password: "비밀번호를 잊으셨나요?" - sign_in_with_omniauth_provider: "%{provider} 으로 로그인" - resend_unlock_instructions: "계정 잠금 해제하기" - resend_confirmation_instructions: "계정 승인 요청하기" + 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 950d138023a..c2eba31c4c5 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -1,121 +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}' - edit_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' - status_tag: - "yes": "Taip" - "no": "Nėra" - 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} %{total} iš viso' - multiple_without_total: 'Rodomi %{model} %{iš} - %{to} ' - entry: - one: 'įrašas' - other: 'įrašai' - any: 'Bet kokia' - blank_slate: - content: 'Nėra %{resource_name}.' - link: 'Sukurti' - dropdown_actions: - button_label: "Veiksmai" + access_denied: + message: Jūs nesate įgaliotas atlikti šį veiksmą. + any: Bet kokia batch_actions: - 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}?' - 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: - resource_type: 'Resurso Tipas' - author_type: 'Autoriaus Tipas' - body: 'Įrašas' - author: 'Autorius' - title: 'Komentaras' - add: 'Pridėti komentarą' - resource: 'Išteklių' - no_comments_yet: 'Dar nėra komentarų.' - author_missing: 'Anonimas' - 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: - username: - title: 'Vartotojo Vardas' + change_password: + submit: Pakeisti mano slaptažodį + title: Slaptažodžio Keitimas email: - title: 'El. paštas' - subdomain: - title: 'Subdomenas' - password: - title: 'Slaptažodis' - sign_up: - title: 'Registracija' - submit: 'Užsiregistruoti' + title: El. paštas + links: + 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: - title: 'Prisijungimas' - remember_me: 'Prisiminti Mane' - submit: 'Prisijungti' + 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: - title: 'Pamiršote slaptažodį?' - submit: 'Sukurti Naują Slaptažodį' - change_password: - title: 'Slaptažodžio Keitimas' - submit: 'Pakeisti mano slaptažodį' + submit: Sukurti Naują Slaptažodį + title: Pamiršote slaptažodį? + sign_up: + submit: Užsiregistruoti + title: Registracija + subdomain: + title: Subdomenas unlock: - title: 'Pakartotinio Atrakinimo Instrukcijos' - submit: 'Pakartotinai siųsti atrakinimo instrukcijas' - resend_confirmation_instructions: - title: 'Patvirtinimo Instrukcijos' - submit: 'Siųsti patvirtinimo instructions' - links: - sign_in: 'Prisijungti' - forgot_your_password: 'Pamiršote slaptažodį?' - sign_in_with_omniauth_provider: 'Prisijungti su %{provider}' - access_denied: - message: 'Jūs nesate įgaliotas atlikti šį veiksmą.' + 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ė" - block: "Sąrašas" - grid: "Tinklelis" - blog: "Blog" + 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} %{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 ef7da02284c..59cf0d48285 100644 --- a/config/locales/lv.yml +++ b/config/locales/lv.yml @@ -1,93 +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" - edit_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" - status_tag: - "yes": "Jā" - "no": "Nē" - 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ā" - multiple_without_total: "%{from} - %{to}" - entry: - one: "ieraksts" - other: "ieraksti" - any: "Jebkurš" - blank_slate: - content: "Sadaļā '%{resource_name}' nav neviena ieraksta." - link: "Izveidot jaunu" - dropdown_actions: - button_label: "Actions" + any: Jebkurš batch_actions: - 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}?" - 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 index e21370ba8d3..c8864992df8 100644 --- a/config/locales/nb.yml +++ b/config/locales/nb.yml @@ -1,116 +1,111 @@ +--- nb: active_admin: - dashboard: Oversikt - dashboard_welcome: - welcome: "Velkommen til Active Admin. Dette er standardoversiktssiden." - call_to_action: "Rediger 'app/admin/dashboard.rb' for å 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" - status_tag: - "yes": "Ja" - "no": "Nei" - main_content: "Vennligst implementer %{model}#main_content for å vise innhold." - logout: "Logg ut" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtere" - pagination: - empty: "Fant ingen %{model}" - one: "Viser 1 %{model}" - one_page: "Viser alle %{n} %{model}" - multiple: "Viser %{model} %{from} - %{to} av %{total} totalt" - multiple_without_total: "Viser %{model} %{from} - %{to}" - entry: - one: "innslag" - other: "innslag" - any: "Alle" - blank_slate: - content: "Her er det ingen %{resource_name} enda." - link: "Opprett en" - dropdown_actions: - button_label: "Handlinger" + access_denied: + message: Du er ikke autorisert til å utføre denne handlingen. + any: Alle batch_actions: - button_label: "Gruppehandlinger" - delete_confirmation: "Er du sikker på at du vil slette disse %{plural_model}? Dette kan ikke reverseres." - succesfully_destroyed: - one: "Slettet én %{model}" - other: "Slettet %{count} %{plural_model}" - selection_toggle_explanation: "(Toggle Selection)" - link: "Opprett en" 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" + 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: - body: "Body" - author: "Author" - title: "Comment" - add: "Add Comment" - resource: "Resource" - no_comments_yet: "No comments yet." - title_content: "Kommentarer (%{count})" + 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." + 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: - username: - title: "Brukernavn" + change_password: + submit: Endre mitt passord + title: Endre passordet email: - title: "E-post" - subdomain: - title: "Subdomene" - password: - title: "Passord" - sign_up: - title: "Opprett brukerkonto" - submit: "Opprett" + title: E-post + links: + forgot_your_password: Glemt passord? + sign_in: Logg inn + sign_in_with_omniauth_provider: Logg på med %{provider} login: - title: "Logg inn" - remember_me: "Husk meg" - submit: "Logg inn" + 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: - title: "Glemt passord?" - submit: "Tilbakestille passordet mitt" - change_password: - title: "Endre passordet" - submit: "Endre mitt passord" + submit: Tilbakestille passordet mitt + title: Glemt passord? + sign_up: + submit: Opprett + title: Opprett brukerkonto + subdomain: + title: Subdomene unlock: - title: "Send info om gjenoppretting på nytt" - submit: "Send info om gjenoppretting på nytt" - resend_confirmation_instructions: - title: "Send bekreftelsesinformasjon på nytt" - submit: "Send bekreftelsesinformasjon på nytt" - links: - sign_in: "Logg inn" - forgot_your_password: "Glemt passord?" - sign_in_with_omniauth_provider: "Logg på med %{provider}" - access_denied: - message: "Du er ikke autorisert til å utføre denne handlingen." + 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" - block: "Liste" - grid: "Gitter" - blog: "Blogg" + 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 661565f2958..07c58f0ba80 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1,134 +1,127 @@ +--- nl: active_admin: - dashboard: Dashboard - dashboard_welcome: - welcome: "Welkom bij Active Admin. Dit is de standaard dashboard pagina" - call_to_action: "Pas uw eigen dashboard aan in het bestand 'app/admin/dashboard.rb'" - view: "Bekijk" - edit: "Wijzig" - delete: "Verwijder" - delete_confirmation: "Weet u zeker dat je dit item wilt verwijderen?" - new_model: "Nieuwe %{model}" - edit_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" - search_status: - headline: "Scope:" - current_filters: "Huidige filters:" - no_current_filters: "Geen" - status_tag: - "yes": "Ja" - "no": "Geen" - 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" - multiple_without_total: "Geeft %{model} %{from} - %{to}" - entry: - one: "entry" - other: "entries" - any: "Alle" - blank_slate: - content: "Er zijn geen %{resource_name} gevonden." - link: "Maak aan" - dropdown_actions: - button_label: "Acties" + access_denied: + message: U bent niet gemachtigd voor deze actie. + any: Alle batch_actions: - 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?" - 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: - created_at: 'Aangemaakt op' - resource_type: "Resource Type" - author_type: "Auteur Type" - body: "Tekst" - author: "Auteur" - title: "Reactie" - add: "Voeg commentaar toe" - delete: 'Verwijder commentaar' - delete_confirmation: "Weet u zeker dat u dit commentaar wilt verwijderen?" - resource: "Resource" - no_comments_yet: "Nog geen reacties." - author_missing: "Anoniem" - 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: - username: - title: "Gebruikersnaam" + change_password: + submit: Mijn wachtwoord wijzigen + title: Wijzig uw wachtwoord email: - title: "Email" - subdomain: - title: "Subdomein" - password: - title: "Wachtwoord" - sign_up: - title: "Registreren" - submit: "Registreren" + title: Email + links: + 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: - title: "inloggen" - remember_me: "Onthoud mij" - submit: "inloggen" + 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: - title: "Wachtwoord vergeten?" - submit: "Reset mijn wachtwoord vergeten" - change_password: - title: "Wijzig uw wachtwoord" - submit: "Mijn wachtwoord wijzigen" + submit: Reset mijn wachtwoord + title: Wachtwoord vergeten? + sign_up: + submit: Registreren + title: Registreren + subdomain: + title: Subdomein unlock: - title: "Verstuur ontgrendelinstructies opnieuw" - submit: "Verstuur ontgrendelinstructies opnieuw" - resend_confirmation_instructions: - title: "Verstuur bevestigingsinstructies opnieuw" - submit: "Verstuur bevestigingsinstructies opnieuw" - links: - sign_up: "Registreren" - sign_in: "Meld u aan" - forgot_your_password: "Wachtwoord vergeten?" - sign_in_with_omniauth_provider: "Log in met %{provider}" - resend_unlock_instructions: "Ontgrendelinstructies opnieuw versturen" - resend_confirmation_instructions: "Bevestigingsinstructies opnieuw versturen" - unsupported_browser: - headline: "Opgelet, ActiveAdmin bied geen support meer voor Internet Explorer 8 of lager" - recommendation: "Wij raden aan om te upgraden naar de nieuwste Internet Explorer, Google Chrome, of Firefox." - turn_off_compatibility_view: "Als u IE 9 of nieuwer gebruikt, zorg ervoor dat u \"Compatibility View\" uit zet." - access_denied: - message: "U bent niet gemachtigd voor deze actie." + 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" - block: "Lijst" - grid: "Rooster" - blog: "Blog" + 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/pl.yml b/config/locales/pl.yml index a1ad50ad4f8..57ae345ab30 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -1,98 +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}" - edit_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ż" - status_tag: - "yes": "Tak" - "no": "Nie" - 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}" - multiple_without_total: "Wyświetlanie %{model} %{from} - %{to}" - any: "Jakikolwiek" - blank_slate: - content: "Nie ma jeszcze zasobu %{resource_name}." - link: "Utwórz go" - dropdown_actions: - button_label: "Akcje" + access_denied: + message: Nie masz uprawnień wystarczających do wykonania tej akcji. + any: Jakikolwiek batch_actions: - button_label: "Akcje na partiach" - default_confirmation: "Czy na pewno chcesz to zrobić?" - delete_confirmation: "Czy na pewno chcesz usunąć te %{plural_model}?" - 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: - sign_up: - title: "Rejestracja" - submit: "Zarejestruj się" - 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" - resend_confirmation_instructions: - title: "Wyślij ponownie instrukcje aktywacji" - submit: "Wyślij ponownie instrukcje aktywacji" + 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}" + 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 e9f50a3ee67..2fbd0b02b4e 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -1,132 +1,148 @@ +--- 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}" - edit_model: "Editar %{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" - search_status: - headline: "Em:" - current_filters: "Filtros escolhidos:" - no_current_filters: "Nenhum" - status_tag: - "yes": "Sim" - "no": "Não" - main_content: "Por favor implemente %{model}#main_content para exibir conteúdo." - logout: "Sair" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtros" - search_status: "Buscou" - 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}" - multiple_without_total: "Exibindo %{model} %{from} - %{to}" - entry: - one: "registro" - other: "registros" - any: "Qualquer" - blank_slate: - content: "Não existem %{resource_name} ainda." - link: "Novo" - dropdown_actions: - button_label: "Ações" + access_denied: + message: Você não tem permissão para realizar o solicitado + any: Qualquer batch_actions: - button_label: "Ações em lote" - default_confirmation: "Tem certeza que quer fazer isso?" - delete_confirmation: "Tem certeza que deseja excluir estes %{plural_model}?" - 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: "Excluir" + 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: - resource_type: "Tipo de Objeto" - author_type: "Tipo de Autor" - body: "Conteúdo" - author: "Autor" - title: "Comentário" - add: "Adicionar Comentário" - resource: "Objeto" - no_comments_yet: "Nenhum comentário." - author_missing: "Anônimo" - 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: - username: - title: "Nome de Usuário" + change_password: + submit: Troque minha senha + title: Troque sua senha email: - title: "E-mail" - subdomain: - title: "Subdomínio" - password: - title: "Senha" - sign_up: - title: "Cadastre-se" - submit: "Continuar" + title: E-mail + links: + 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: - title: "Conta" - remember_me: "Lembrar da senha" - submit: "Entrar" + 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: - title: "Esqueceu sua senha?" - submit: "Reinicie minha senha" - change_password: - title: "Troque sua senha" - submit: "Troque minha senha" + submit: Reinicie minha senha + title: Esqueceu sua senha? + sign_up: + submit: Continuar + title: Cadastre-se + subdomain: + title: Subdomínio unlock: - title: "Reenviar instruções de desbloqueio" - submit: "Reenviar instruções de desbloqueio" - resend_confirmation_instructions: - title: "Reenviar instruções de confirmação" - submit: "Reenviar instruções de confirmação" - links: - sign_up: "Criar conta" - sign_in: "Entrar" - forgot_your_password: "Esqueceu sua senha?" - sign_in_with_omniauth_provider: "Entre com o %{provider}" - resend_unlock_instructions: "Reenviar instruções de desbloqueio" - resend_confirmation_instructions: "Reenviar instruções de confirmação" - unsupported_browser: - headline: "O ActiveAdmin não oferece suporte ao Internet Explorer versão 8 ou inferior." - recommendation: "Nós recomendamos atualizar para a última versão do Internet Explorer, Google Chrome, ou Firefox." - turn_off_compatibility_view: "Se você está usando o IE 9 ou superior, desligue o \"Modo de Exibição de Compatibilidade\"." - access_denied: - message: "Você não tem permissão para realizar o solicitado" + 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" - block: "Lista" - grid: "Grid" - blog: "Blog" + 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 f9216bd25fd..729048d585d 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -1,93 +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}" - edit_model: "Editar %{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" - status_tag: - "yes": "Sim" - "no": "Não" - 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}" - multiple_without_total: "Mostrando %{model} %{from} - %{to}" - entry: - one: "registro" - other: "registros" - any: "Qualquer" - blank_slate: - content: "Ainda não existem %{resource_name}." - link: "Novo" - dropdown_actions: - button_label: "Ações" + any: Qualquer batch_actions: - 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}?" - 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: "Excluir" + 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 79c889160df..e89a5444fbb 100644 --- a/config/locales/ro.yml +++ b/config/locales/ro.yml @@ -1,97 +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}" - edit_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" - status_tag: - "yes": "Da" - "no": "Nu" - 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" - multiple_without_total: "Sunt afisate %{from} - %{to}" - entry: - one: "inregistrare" - other: "inregistrari" - any: "Oricare" - blank_slate: - content: "Momentan nu exista %{resource_name}." - link: "Creati un" - dropdown_actions: - button_label: "Actiuni" + any: Oricare batch_actions: - 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}?" - 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 2eda9b94d84..74064738953 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -1,138 +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}" - 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: "меньше" - search_status: - headline: "Область:" - current_filters: "Текущий фильтр:" - no_current_filters: "Ни один" - status_tag: - "yes": "Да" - "no": "Нет" - main_content: "Создайте %{model}#main_content для отображения содержимого." - logout: "Выйти" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Фильтры" - search_status: "Статус поиска" - pagination: - empty: "%{model} не найдено" - one: "Результат: 1 %{model}" - one_page: "Результат: %{n} %{model}" - multiple: "Результат: %{model} %{from} - %{to} из %{total}" - multiple_without_total: "Результат: %{model} %{from} - %{to}" - entry: - one: "запись" - few: "записи" - many: "записей" - other: "записей" - any: "Любой" - blank_slate: - content: "Пока нет %{resource_name}." - link: "Создать" - dropdown_actions: - button_label: "Oперации" + access_denied: + message: Вы не авторизованы для выполнения данного действия. + any: Любой batch_actions: - button_label: "Групповые операции" - default_confirmation: "Вы уверены, что вы хотите это сделать?" - 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: "Добавить Комментарий" - delete: "Удалить Комментарий" - delete_confirmation: "Вы уверены, что хотите удалить этот комментарий?" - resource: "Ресурс" - no_comments_yet: "Пока нет комментариев." - author_missing: "Аноним" - 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: - username: - title: "Имя пользователя" + change_password: + submit: Изменение пароля + title: Изменение пароля email: - title: "Эл. почта" - subdomain: - title: "Поддомен" - password: - title: "Пароль" - sign_up: - title: "Зарегистрироваться" - submit: "Зарегистрироваться" + 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: "Повторно отправить инструкции по разблокировке" - resend_confirmation_instructions: - title: "Выслать повторно письмо с активацией" - submit: "Выслать повторно письмо с активацией" - links: - sign_up: "Зарегистрироваться" - sign_in: "Войти" - forgot_your_password: "Забыли пароль?" - sign_in_with_omniauth_provider: "Войти с помощью %{provider}" - resend_unlock_instructions: "Повторная отправка инструкций разблокировки" - resend_confirmation_instructions: "Повторная отправка инструкций подтверждения" - unsupported_browser: - headline: "Пожалуйста, обратите внимание, что ActiveAdmin больше не поддерживает Internet Explorer 8 версии и старее" - recommendation: "Мы рекомендуем обновить версию вашего браузера (Internet Explorer, Google Chrome, или Firefox)." - turn_off_compatibility_view: "Если вы используете IE 9 или новее, убедитесь, что вы выключили опцию \"Просмотр в режиме совместимости\"." - 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 0b7d4d6d6f3..647c23896da 100644 --- a/config/locales/sv-SE.yml +++ b/config/locales/sv-SE.yml @@ -1,133 +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: "Redigera" - delete: "Ta bort" - delete_confirmation: "Är du säker att du vill ta bort denna?" - new_model: "Ny %{model}" - edit_model: "Redigera %{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: "Rensa filter" - 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" - search_status: - headline: "Scope:" - current_filters: "Nuvarande filter:" - no_current_filters: "Inga" - status_tag: - "yes": "Ja" - "no": "Nej" - 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" - search_status: "Sök status" - 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" - multiple_without_total: "Visar %{model} %{from} - %{to}" - entry: - one: "inlägg" - other: "inlägg" - any: "Någon" - blank_slate: - content: "Finns inga %{resource_name} än." - link: "Skapa en" - dropdown_actions: - button_label: "Behandling" + access_denied: + message: Du har inte behörighet att utföra denna åtgärd. + any: Alla batch_actions: - button_label: "Batch behandling" - 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}?" - 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: - created_at: "Skapad" - resource_type: "Resurs typ" - author_type: "Författar typ" - body: "Innehåll" - author: "Författare" - title: "Kommentar" - add: "Lägg till kommentar" - resource: "Resurs" - no_comments_yet: "Inga kommentarer än." - author_missing: "Anonym" - 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: - username: - title: "Användarnamn" + change_password: + submit: Ändra mitt lösenord + title: Ändra ditt lösenord email: - title: "Epost" - subdomain: - title: "Subdomän" - password: - title: "Lösenord" - sign_up: - title: "Registera" - submit: "Registera" + title: E-post + links: + 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: - title: "Inloggning" - remember_me: "Kom ihåg mig" - submit: "Inloggning" + 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: - 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: Återställ mitt lösenord + title: Glömt ditt lösenord? + sign_up: + submit: Registera + title: Registera + subdomain: + title: Subdomän unlock: - title: "Skicka upplåsnings instruktioner" - submit: "Skicka upplåsnings instruktioner" - resend_confirmation_instructions: - title: "Skicka bekräftnings instruktioner" - submit: "Skicka bekräftnings instruktioner" - links: - sign_up: "Registera" - sign_in: "Logga in" - forgot_your_password: "Glömt ditt lösenord?" - sign_in_with_omniauth_provider: "Logga in med %{provider}" - resend_unlock_instructions: "Skicka upplåsnings instruktioner" - resend_confirmation_instructions: "Skicka bekräftnings instruktioner" - unsupported_browser: - headline: "Notera att ActiveAdmin inte längre stödjer Internet Explorer version 8 eller mindre." - recommendation: "Vi rekommenderar dig att uppgradera till den senaste versionen av Internet Explorer, Google Chrome, eller Firefox." - turn_off_compatibility_view: "Om du använder IE 9 eller senare, se till att stäng av \"Compatibility View\"." - access_denied: - message: "Du har inte rättighet att utföra denna åtgärd." + 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" - block: "Lista" - grid: "Rutnät" - blog: "Blogg" + 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 6f0070b43f5..8f8c647912c 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1,97 +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}" - edit_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" - status_tag: - "yes": "Evet" - "no": "Hayır" - 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" - multiple_without_total: "%{model}. %{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" - dropdown_actions: - button_label: "Işlemler" + access_denied: + message: Bu işlemi gerçekleştirmek için yetkiniz yok. + any: Herhangi biri batch_actions: - button_label: "Toplu işlemler" - default_confirmation: "Bunu yapmak istediğinden emin misin?" - delete_confirmation: "%{plural_model} kayıtlarını silmek istediğinize emin misiniz?" - 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" - resend_confirmation_instructions: - title: "Onaylama talimatlarını tekrar gönder" - submit: "Onaylama talimatlarını tekrar gönder" + 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 509bd482b07..5cf78bb6640 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -1,136 +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}" - 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: "меньше" - search_status: - headline: "Область:" - current_filters: "Поточний фільтр:" - no_current_filters: "Жоден" - status_tag: - "yes": "Так" - "no": "Ні" - main_content: "Створіть %{model}#main_content для відображення вмісту." - logout: "Вийти" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Фільтри" - search_status: "Статус пошуку" - pagination: - empty: "%{model} не знайдено" - one: "Результат: 1 %{model}" - one_page: "Результат: %{n} %{model}" - multiple: "Результат: %{model} %{from} - %{to} з %{total}" - multiple_without_total: "Результат: %{model} %{from} - %{to}" - entry: - one: "запис" - few: "записи" - many: "записів" - other: "записів" - any: "Будь-який" - blank_slate: - content: "Покищо немає %{resource_name}." - link: "Створити" - dropdown_actions: - button_label: "Oперації" + access_denied: + message: Ви не авторизовані для виконання даної дії. + any: Будь-який batch_actions: - button_label: "Групові операції" - default_confirmation: "Ви справді бажаєте це зробити?" - 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: "Покищо немає коментарів." - author_missing: "Анонім" - 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: - username: - title: "Ім'я користувача" + change_password: + submit: Змінити пароль + title: Зміна паролю email: - title: "Ел. пошта" - subdomain: - title: "Піддомен" - password: - title: "Пароль" - sign_up: - title: "Зареєструватися" - submit: "Зареєструватися" + 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: "Відправити повторно інструкції з розблокування" - resend_confirmation_instructions: - title: "Вислати повторно листа з активацією" - submit: "Вислати повторно листа з активацією" - links: - sign_up: "Зареєструватись" - sign_in: "Увійти" - forgot_your_password: "Забули пароль?" - sign_in_with_omniauth_provider: "Увійти з допомогою %{provider}" - resend_unlock_instructions: "Повторна відправка інструкцій розблокування" - resend_confirmation_instructions: "Повторна відправка інструкцій підтвердження" - unsupported_browser: - headline: "Зверніть, будь-ласка, увагу, що ActiveAdmin більше не підтримує Internet Explorer 8 версії і нижче" - recommendation: "Ми рекомендуємо оновити версію вашого браузеру (Internet Explorer, Google Chrome, або Firefox)." - turn_off_compatibility_view: "Якщо ви використовуєте IE 9 і вище переконайтесь, що ви виключили опцію \"Перегляд в режимі сумісності\"." - 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 b45569765f2..ae56a3641a7 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -1,94 +1,139 @@ +--- 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}" - edit_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" - status_tag: - "yes": "Có" - "no": "Không Có" - 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ả." - multiple_without_total: "Đang hiển thị %{model} %{from} - %{to}." - entry: - one: "entry" - other: "entries" - any: "Bất kỳ" - blank_slate: - content: "Chưa có %{resource_name}." - link: "Tạo mới" - dropdown_actions: - button_label: "Hành động" + 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" - 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}?" - 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}. + one: Đang hiển thị 1 %{model} + one_page: Đang hiển thị tất cả %{n} %{model} + per_page: 'Mỗi trang: ' + 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ó + 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ó + 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 ee3fa7ec335..db63e704970 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -1,111 +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}" - 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: "小于" - search_status: - headline: "搜索范围:" - current_filters: "过滤条件:" - no_current_filters: "无" - status_tag: - "yes": "是的" - "no": "无" - main_content: "请执行 %{model}#main_content 来显示内容." - logout: "退出" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "所有条件" - search_status: "搜索条件" - pagination: - empty: "暂时没有%{model}" - one: "显示 1 %{model}" - one_page: "显示 所有 %{n} %{model}" - multiple: "显示所有 %{total} %{model}中的%{from} - %{to} 条" - multiple_without_total: "%{model}中的%{from} - %{to} 条" - entry: - one: "条目" - other: "条目" - any: "任何" - blank_slate: - content: "暂时还没有%{resource_name}." - link: "新建一个" - dropdown_actions: - button_label: "行动" + access_denied: + message: 您无权处理此操作 + any: 任何 batch_actions: - button_label: "批处理" - default_confirmation: "你确定你要这样做?" - 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: - username: - title: "用户名" + change_password: + submit: 修改密码 + title: 修改密码 email: - title: "邮箱" - subdomain: - title: "子域" - password: - title: "密码" + 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: "重置我的密码" + submit: 重置我的密码 + title: 忘记了密码? + sign_up: + submit: 注册 + title: 注册 + subdomain: + title: 子域 unlock: - title: "重新发送送解锁命令" - submit: "重新发送送解锁命令" - resend_confirmation_instructions: - 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: 表格 + 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 67d14f79693..65ccc8f8977 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -1,127 +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}" - 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: "小於" - search_status: - headline: "子集:" - current_filters: "目前篩選條件:" - no_current_filters: "無" - status_tag: - "yes": "是" - "no": "否" - main_content: "請實作 %{model}#main_content 以顯示內容。" - logout: "登出" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "篩選條件" - search_status: "搜尋條件" - pagination: - empty: "找不到 %{model} " - one: "顯示 1 %{model}" - one_page: "顯示 全部 %{n} %{model}" - multiple: "總計 %{total} 顯示 %{model} 中%{from} - %{to} 筆" - multiple_without_total: "顯示 %{model} 中%{from} - %{to} 筆" - entry: - one: "筆" - other: "筆" - any: "任何" - blank_slate: - content: "尚無 %{resource_name}" - link: "建立一筆" - dropdown_actions: - button_label: "操作" + access_denied: + message: 你沒有權限執行此項操作 + any: 任何 batch_actions: - button_label: "批次作業" - default_confirmation: "你確定你要這樣做?" - 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: "尚無評論" - author_missing: "匿名" - 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: - username: - title: "帳號" + change_password: + submit: 更改我的密碼 + title: 更改你的密碼 email: - title: "電子郵件信箱" - subdomain: - title: "子網域" - password: - title: "密碼" - sign_up: - title: "註冊" - submit: "註冊" + 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: "重新發送解鎖指示" - resend_confirmation_instructions: - title: '重新發送確認信' - submit: '重新發送確認信' - links: - sign_up: "註冊" - sign_in: "登入" - forgot_your_password: "忘記密碼?" - sign_in_with_omniauth_provider: "使用 %{provider} 登入" - resend_unlock_instructions: "重新發送解鎖指示" - resend_confirmation_instructions: "重新發送確認信" - unsupported_browser: - headline: "很抱歉,ActiveAdmin 已不再支援 Internet Explorer 8 以下版本的瀏覽器。" - recommendation: "建議您升級到最新版本的Internet ExplorerGoogle Chrome,或是 Firefox。" - turn_off_compatibility_view: "若您是使用 IE 9 或更新的版本,請確認「相容性檢視」是關閉的。" - 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: 表格 + 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..3bd37b28c43 --- /dev/null +++ b/docs/.vitepress/config.js @@ -0,0 +1,94 @@ +import { version } from '../../package.json' +import { defineConfig } from 'vitepress' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "ActiveAdmin", + description: "The administration framework for business critical Ruby on Rails applications.", + head: [ + // ['link', { rel: 'icon', type: 'image/svg+xml', href: '/vitepress-logo-mini.svg' }], + // ['link', { rel: 'icon', type: 'image/png', href: '/vitepress-logo-mini.png' }], + // ['meta', { name: 'theme-color', content: '#5f67ee' }], + ['meta', { name: 'og:type', content: 'website' }], + ['meta', { name: 'og:locale', content: 'en' }], + ['meta', { name: 'og:site_name', content: 'ActiveAdmin' }], + // ['meta', { name: 'og:image', content: 'https://vitepress.dev/vitepress-og.jpg' }], + ], + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + { text: 'Guide', link: '/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: version.replace("-", "."), // use Ruby format for version text + items: [ + { + text: 'Changelog', + link: 'https://github.com/activeadmin/activeadmin/releases', + }, + { + text: 'Contributing', + link: 'https://github.com/activeadmin/activeadmin/blob/master/CONTRIBUTING.md', + }, + ], + } + ], + sidebar: [ + { + text: 'Setup', + items: [ + { text: 'Installation', link: '/0-installation' }, + { text: 'Configuration', link: '/1-general-configuration' } + ] + }, + { + text: 'Resources', + items: [ + { text: 'Working with Resources', link: '/2-resource-customization' }, + { text: 'Customize the Index page', link: '/3-index-pages' }, + { text: 'Index as a Table', link: '/3-index-pages/index-as-table' }, + { text: 'Custom Index View', link: '/3-index-pages/custom-index' }, + { text: 'CSV Format', link: '/4-csv-format' }, + { text: 'Forms', link: '/5-forms' }, + { text: 'Customize the Show Page', link: '/6-show-pages' }, + { text: 'Sidebar Sections', link: '/7-sidebars' }, + { text: 'Custom Controller Actions', link: '/8-custom-actions' }, + { text: 'Batch Actions', link: '/9-batch-actions' }, + { text: 'Decorators', link: '/11-decorators' }, + { text: 'Authorization Adapter', link: '/13-authorization-adapter' } + ] + }, + { + text: 'Other', + items: [ + { text: 'Custom Pages', link: '/10-custom-pages' }, + { text: 'Arbre Components', link: '/12-arbre-components' }, + { text: 'Gotchas', link: '/14-gotchas' }, + { text: 'Documentation Tips', link: '/markdown-examples' }, + ] + } + ], + socialLinks: [ + { icon: 'github', link: 'https://github.com/activeadmin/activeadmin' }, + { icon: 'slack', link: 'https://activeadmin.slack.com/' }, + ], + editLink: { + pattern: 'https://github.com/activeadmin/activeadmin/edit/master/docs/:path', + text: 'Edit this page on GitHub' + }, + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2010-present' + }, + search: { + provider: 'local' + } + } +}) diff --git a/docs/0-installation.md b/docs/0-installation.md index 3b4c8466185..c12951ef43e 100644 --- a/docs/0-installation.md +++ b/docs/0-installation.md @@ -1,3 +1,7 @@ +--- +redirect_from: /docs/0-installation.html +--- + # Installation Active Admin is a Ruby Gem. @@ -7,41 +11,48 @@ gem 'activeadmin' # Plus integrations with: gem 'devise' -gem 'cancan' # or cancancan +gem 'cancancan' gem 'draper' gem 'pundit' ``` -More accurately, it's a [Rails Engine](http://guides.rubyonrails.org/engines.html) +More accurately, it's a [Rails Engine](https://guides.rubyonrails.org/engines.html) that can be injected into your existing Ruby on Rails application. ## Setting up Active Admin -After installing the gem, you need to run the generator. By default we use Devise, and -the generator creates an `AdminUser` model. If you want to create a different model -(or modify an existing one for use with Devise) you can pass it as an argument. -If you want to skip Devise configuration entirely, you can pass `--skip-users`. +After installing the gem, you need to run the generator. Here are your options: -```sh -rails g active_admin:install # creates the AdminUser class -rails g active_admin:install User # creates / edits the class for use with Devise -rails g active_admin:install --skip-users # skips Devise install -``` +* If you don't want to use Devise, run it with `--skip-users`: + + ```sh + rails g active_admin:install --skip-users + ``` + +* If you want to customize the name of the generated user class, or if you want to use an existing user class, provide the class name as an argument: + + ```sh + rails g active_admin:install User + ``` + +* Otherwise, with no arguments we will create an `AdminUser` class to use with Devise: + + ```sh + rails g active_admin:install + ``` The generator adds these core files, among others: -``` -app/admin/dashboard.rb -app/assets/javascripts/active_admin.js.coffee -app/assets/stylesheets/active_admin.scss -config/initializers/active_admin.rb -``` +* `app/admin/dashboard.rb` +* `app/assets/javascripts/active_admin.js` +* `app/assets/stylesheets/active_admin.scss` +* `config/initializers/active_admin.rb` Now, migrate and seed your database before starting the server: ```sh -rake db:migrate -rake db:seed +rails db:migrate +rails db:seed rails server ``` @@ -55,16 +66,17 @@ Voila! You're on your brand new Active Admin dashboard. To register an existing model with Active Admin: ```sh -rails generate active_admin:resource MyModel +rails generate active_admin:resource Post ``` -This creates a file at `app/admin/my_model.rb` to set up the UI; refresh your browser to see it. +This creates a `app/admin/post.rb` file with some content to start. Preview +any changes in your browser. # Upgrading When upgrading to a new version, it's a good idea to check the [CHANGELOG]. -To update the JS & CSS assets: +To update the assets: ```sh rails generate active_admin:assets @@ -75,6 +87,8 @@ You should also sync these files with their counterparts in the AA source code: * app/admin/dashboard.rb [~>][dashboard.rb] * config/initializers/active_admin.rb [~>][active_admin.rb] +Along with any template partials you've copied and modified. + # Gem compatibility ## will_paginate @@ -89,8 +103,8 @@ Kaminari.configure do |config| end ``` -If you are also using [Draper](https://github.com/drapergem/draper), you may want to -make sure `per_page_kaminari` is delegated correctly: +If you are also using [Draper](https://github.com/drapergem/draper), you may +want to make sure `per_page_kaminari` is delegated correctly: ```ruby Draper::CollectionDecorator.send :delegate, :per_page_kaminari @@ -100,6 +114,41 @@ Draper::CollectionDecorator.send :delegate, :per_page_kaminari If you're getting the error `wrong number of arguments (6 for 4..5)`, [read #2703]. +## webpacker + +You can **opt-in to using Webpacker for ActiveAdmin assets** as well by updating your configuration to turn on the `use_webpacker` option, either at installation time or manually. + +* at active_admin installation: + + ```sh + rails g active_admin:install --use_webpacker + ``` + +* manually: + + ```ruby + ActiveAdmin.setup do |config| + config.use_webpacker = true + end + ``` + + And run the generator to get default Active Admin assets: + + ```sh + rails g active_admin:webpacker + ``` + +## vite_rails + +To use Active Admin with Vite, make sure the `@activeadmin/activeadmin` dependency is added to your `package.json` using e.g. Yarn: + +```sh +yarn add @activeadmin/activeadmin@^3 +``` + +Then follow the steps outlined in this discussion comment: https://github.com/activeadmin/activeadmin/discussions/7947#discussioncomment-5867902 + + [CHANGELOG]: https://github.com/activeadmin/activeadmin/blob/master/CHANGELOG.md [dashboard.rb]: https://github.com/activeadmin/activeadmin/blob/master/lib/generators/active_admin/install/templates/dashboard.rb [active_admin.rb]: https://github.com/activeadmin/activeadmin/blob/master/lib/generators/active_admin/install/templates/active_admin.rb.erb diff --git a/docs/1-general-configuration.md b/docs/1-general-configuration.md index 6cb1ba8a754..ac4bfb5b24a 100644 --- a/docs/1-general-configuration.md +++ b/docs/1-general-configuration.md @@ -1,3 +1,7 @@ +--- +redirect_from: /docs/1-general-configuration.html +--- + # General Configuration You can configure Active Admin settings in `config/initializers/active_admin.rb`. @@ -36,17 +40,28 @@ If you want, you can customize it. config.site_title = "My Admin Site" config.site_title_link = "/" config.site_title_image = "site_image.png" -config.site_title_image = "http://www.google.com/images/logos/google_logo_41.png" +config.site_title_image = "https://www.google.com/images/logos/google_logo_41.png" config.site_title_image = ->(context) { context.current_user.company.logo_url } ``` ## Internationalization (I18n) -To translate Active Admin to a new language or customize an existing translation, you can copy +Active Admin comes with translations for a lot of +[locales](https://github.com/activeadmin/activeadmin/blob/master/config/locales/). +Active Admin does not provide the translations for the kaminari gem it uses for pagination, +to get these you can use the +[kaminari-i18n](https://github.com/tigrish/kaminari-i18n) gem. + +To translate Active Admin to a new language or customize an existing +translation, you can copy [config/locales/en.yml](https://github.com/activeadmin/activeadmin/blob/master/config/locales/en.yml) -to your application's `config/locales` folder and update it. We welcome new/updated translations, -so feel free to [contribute](https://github.com/activeadmin/activeadmin/blob/master/CONTRIBUTING.md)! -To translate third party gems like devise, use for example devise-i18n. +to your application's `config/locales` folder and update it. We welcome +new/updated translations, so feel free to +[contribute](https://github.com/activeadmin/activeadmin/blob/master/CONTRIBUTING.md)! + +When using [devise](https://github.com/plataformatec/devise) for authentication, +you can use the [devise-i18n](https://github.com/tigrish/devise-i18n) +gem to get the devise translations for other locales. ## Localize Format For Dates and Times @@ -91,6 +106,26 @@ ActiveAdmin.setup do |config| end ``` +If you are creating a multi-tenant application you may want to have multiple namespaces mounted to the same path. We can do this using the `route_options` settings on the namespace + +```ruby +config.namespace :site_1 do |admin| + admin.route_options = { path: :admin, constraints: ->(request){ request.domain == "site1.com" } } +end + +config.namespace :site_2 do |admin| + admin.route_options = { path: :admin, constraints: ->(request){ request.domain == "site2.com" } } +end +``` + +If you would like to mount the namespace to a subdomain instead of path we can use the `route_options` for this as well + +```ruby +config.namespace :admin do |admin| + admin.route_options = { path: '', subdomain: 'admin' } +end +``` + Each setting available in the Active Admin setup block is configurable on a per namespace basis. @@ -107,8 +142,7 @@ end ## Comments -By default Active Admin includes comments on resources. Sometimes, this is -undesired. To disable comments: +By default Active Admin includes comments on resources. To disable comments: ```ruby # For the entire application: @@ -129,6 +163,37 @@ ActiveAdmin.register Post do end ``` +You can change the name under which comments are registered: + +```ruby +config.comments_registration_name = 'AdminComment' +``` + +You can change the order for the comments and you can change the column to be +used for ordering: + +```ruby +config.comments_order = 'created_at ASC' +``` + +You can disable the menu item for the comments index page: + +```ruby +config.comments_menu = false +``` + +You can customize the comment menu: + +```ruby +config.comments_menu = { parent: 'Admin', priority: 1 } +``` + +Remember to indicate where to place the comments and form with: + +```ruby +active_admin_comments_for(resource) +``` + ## Utility Navigation The "utility navigation" shown at the top right normally shows the current user @@ -139,11 +204,20 @@ menu in the system; you can provide your own menu to be rendered in its place. ActiveAdmin.setup do |config| config.namespace :admin do |admin| admin.build_menu :utility_navigation do |menu| - menu.add label: "ActiveAdmin.info", url: "http://www.activeadmin.info", - html_options: { target: :blank } + menu.add label: "ActiveAdmin.info", url: "https://www.activeadmin.info", + html_options: { target: "_blank" } admin.add_current_user_to_menu menu admin.add_logout_button_to_menu menu end end end ``` + +## Footer Customization + +By default, Active Admin displays a "Powered by ActiveAdmin" message on every +page. You can override this message and show domain-specific messaging: + +```ruby +config.footer = "MyApp Revision v1.3" +``` diff --git a/docs/10-custom-pages.md b/docs/10-custom-pages.md index 12332c63532..9b91445247a 100644 --- a/docs/10-custom-pages.md +++ b/docs/10-custom-pages.md @@ -1,3 +1,7 @@ +--- +redirect_from: /docs/10-custom-pages.html +--- + # Custom Pages If you have data you want on a standalone page that isn't tied to a resource, @@ -49,7 +53,7 @@ end See the [Menu](2-resource-customization.md#customize-the-menu) documentation. -## Customize the breadcrumbs +## Customize the breadcrumbs ```ruby ActiveAdmin.register_page "Calendar" do @@ -71,6 +75,20 @@ ActiveAdmin.register_page "Calendar", namespace: :today ActiveAdmin.register_page "Calendar", namespace: false ``` +## Belongs To + +To nest the page within another resource, you can use the `belongs_to` method: + +```ruby +ActiveAdmin.register Project +ActiveAdmin.register_page "Status" do + belongs_to :project +end +``` + +See also the [Belongs To](2-resource-customization.md#belongs-to) documentation +and examples. + ## Add a Sidebar See the [Sidebars](7-sidebars.md) documentation. @@ -88,7 +106,8 @@ end ## Add a Page Action -Page actions are custom controller actions (which mirror the resource DSL for the same feature). +Page actions are custom controller actions (which mirror the resource DSL for +the same feature). ```ruby page_action :add_event, method: :post do @@ -103,4 +122,29 @@ end This defines the route `/admin/calendar/add_event` which can handle HTTP POST requests. -Clicking on the action item will reload page and display the message "Your event was added" +Clicking on the action item will reload page and display the message "Your event +was added" + +Page actions can handle multiple HTTP verbs. + +```ruby +page_action :add_event, method: [:get, :post] do + # ... +end +``` + +See also the [Custom Actions](8-custom-actions.md#http-verbs) example. + +## Use custom column as id + +You can use custom parameter instead of id + +```ruby +ActiveAdmin.register User do + controller do + defaults :finder => :find_by_name + end +end +``` + +This defines the resource route as `/admin/users/john` if user name is john diff --git a/docs/11-decorators.md b/docs/11-decorators.md index fc11bbe05b2..75fe96c64b1 100644 --- a/docs/11-decorators.md +++ b/docs/11-decorators.md @@ -1,14 +1,13 @@ +--- +redirect_from: /docs/11-decorators.html +--- + # Decorators Active Admin allows you to use the decorator pattern to provide view-specific versions of a resource. [Draper](https://github.com/drapergem/draper) is recommended but not required. -To use decorator support without Draper, your decorator must support a variety -of collection methods to support pagination, filtering, etc. See -[this github issue discussion](https://github.com/activeadmin/activeadmin/issues/3600) -and [this gem](https://github.com/kiote/activeadmin-poro-decorator) for more details. - ## Example usage ```ruby @@ -38,6 +37,61 @@ ActiveAdmin.register Post do end ``` +You can pass any decorator class as an argument to `decorate_with` +as long as it accepts the record to be decorated as a parameter in +the initializer, and responds to all the necessary methods. + +```ruby +# app/decorators/post_decorator.rb +class PostDecorator + attr_reader :post + delegate_missing_to :post + + def initialize(post) + @post = post + end +end +``` + +If a given resource uses ActiveAdmin's Comments feature, then that resource's decorator class must respond to +`model` where it returns the model instance and `decorated?` returns `true`. + +```ruby +# app/decorators/post_decorator.rb +class PostDecorator + attr_reader :post + delegate_missing_to :post + + def initialize(post) + @post = post + end + + def decorated? + true + end + + def model + post + end +end +``` + +If you use any actions with param(e.g. show, edit, destroy), your decorator +class must explicitly delegate `to_param` to the decorated model. + +```ruby +# app/decorators/post_decorator.rb +class PostDecorator + attr_reader :post + delegate_missing_to :post + delegate :to_param, to: :post + + def initialize(post) + @post = post + end +end +``` + ## Forms By default, ActiveAdmin does *not* decorate the resource used to render forms. diff --git a/docs/12-arbre-components.md b/docs/12-arbre-components.md index 7c411f20c9c..13b14da38ce 100644 --- a/docs/12-arbre-components.md +++ b/docs/12-arbre-components.md @@ -1,3 +1,7 @@ +--- +redirect_from: /docs/12-arbre-components.html +--- + # Arbre Components Arbre allows the creation of shareable and extendable HTML components and is @@ -18,7 +22,7 @@ ActiveAdmin.register Post do row :id row 'Tags' do post.tags.each do |tag| - a tag, href: admin_post_path(q: {tagged_with_contains: tag}) + a tag, href: admin_post_path(q: { tagged_with_cont: tag }) text_node " ".html_safe end end @@ -34,16 +38,16 @@ A panel is a component that takes up all available horizontal space and takes a title and a hash of attributes as arguments. If a sidebar is present, a panel will take up the remaining space. -This will create two stacked panels: +This will create two vertically stacked panels: ```ruby show do panel "Post Details" do - render partial: "details", locals: {post: post} + render partial: "details", locals: { post: post } end panel "Post Tags" do - render partial: "tags", locals: {post: post} + render partial: "tags", locals: { post: post } end end ``` @@ -54,9 +58,9 @@ 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 +### Simple Columns -To create simple columnns, use the `columns` method. Within the block, call +To create simple columns, use the `columns` method. Within the block, call the #column method to create a new column. ```ruby @@ -111,6 +115,21 @@ end In the above example, the first column will not grow larger than 200px and will not shrink less than 100px. +### Custom Column Class + +Pass the `:class` option to the column method to set a custom class. + +```ruby +columns do + column class: "important" do + span "Column # 1" + end + column do + span "Column # 2" + end +end +``` + ## Table For Table For provides the ability to create tables like those present @@ -120,31 +139,78 @@ uses `column` to build the fields to show with the table. ```ruby table_for order.payments do column(:payment_type) { |payment| payment.payment_type.titleize } - column "Received On", :created_at + column "Received On", :created_at column "Details & Notes", :payment_details - column "Amount", :amount_in_dollars + column "Amount", :amount_in_dollars end ``` -the `column` method can take a title as its first argument and data +The `column` method can take a title as its first argument and data (`:your_method`) as its second (or first if no title provided). Column also takes a block. +### Internationalization + +To customize the internationalization for the component, specify a resource to +use for translations via the `i18n` named parameter. This is only necessary for +non-`ActiveRecord::Relation` collections: + +```ruby +table_for payments, i18n: Payment do + # ... +end +``` + ## Status tag Status tags provide convenient syntactic sugar for styling items that have status. A common example of where the status tag could be useful is for orders that are complete or in progress. `status_tag` takes a status, like -"In Progress", a type, which defaults to nil, and a hash of options. The -status_tag will generate html markup that Active Admin css uses in styling. +"In Progress", and a hash of options. The status_tag will generate HTML markup +that Active Admin CSS uses in styling. ```ruby status_tag 'In Progress' # => In Progress -status_tag 'active', :ok -# => Active +status_tag 'active', class: 'important', id: 'status_123', label: 'on' +# => on +``` + +When providing a `true` or `false` value, the `status_tag` will display "Yes" +or "No". This can be configured through the `"en.active_admin.status_tag"` +locale. -status_tag 'active', :ok, class: 'important', id: 'status_123', label: 'on' -# => on +```ruby +status_tag true +# => Yes ``` + +In the case that a boolean field is `nil`, it will display "No" as a default. +But using the `"en.active_admin.status_tag.unset"` locale key, it can be +configured to display something else. + +## Tabs + +The Tabs component is helpful for saving page real estate. The first tab will be +the one open when the page initially loads and the rest hidden. You can click +each tab to toggle back and forth between them. Arbre supports unlimited number +of tabs. + +```ruby +tabs do + tab :active do + table_for orders.active do + # ... + end + end + + tab :inactive, html_options: { class: "specific_css_class" } do + table_for orders.inactive do + # ... + end + end +end +``` + +The `html_options` will set additional HTML attributes on the tab button. diff --git a/docs/13-authorization-adapter.md b/docs/13-authorization-adapter.md index 33346ee6110..326cb0a128f 100644 --- a/docs/13-authorization-adapter.md +++ b/docs/13-authorization-adapter.md @@ -1,3 +1,7 @@ +--- +redirect_from: /docs/13-authorization-adapter.html +--- + # Authorization Adapter Active Admin offers the ability to define and use your own authorization @@ -6,8 +10,8 @@ taken. By default, '#authorized?' returns true. ## Setting up your own AuthorizationAdapter -Setting up your own `AuthorizationAdapter` is easy! The following example shows -how to set up and tie your authorization adapter class to Active Admin: +The following example shows how to set up and tie your authorization +adapter class to Active Admin: ```ruby # app/models/only_authors_authorization.rb @@ -37,7 +41,11 @@ application's `config/initializers/active_admin.rb` and add/modify the line: config.authorization_adapter = "OnlyAuthorsAuthorization" ``` -Authorization adapters can be configured per ActiveAdmin namespace as well, for example: +Now, whenever a controller action is performed, the `OnlyAuthorsAuthorization`'s +`#authorized?` method will be called. + +Authorization adapters can be configured per ActiveAdmin namespace as well, for +example: ```ruby ActiveAdmin.setup do |config| @@ -50,9 +58,6 @@ ActiveAdmin.setup do |config| end ``` -Now, whenever a controller action is performed, the `OnlyAuthorsAuthorization`'s -`#authorized?` method will be called. - ## Getting Access to the Current User From within your authorization adapter, you can call the `#user` method to @@ -70,9 +75,9 @@ end ## Scoping Collections in Authorization Adapters -`ActiveAdmin::AuthorizationAdapter` also provides a hook method (`#scope_collection`) -for the adapter to scope the resource's collection. For example, you may want to -centralize the scoping: +`ActiveAdmin::AuthorizationAdapter` also provides a hook method +(`#scope_collection`) for the adapter to scope the resource's collection. For +example, you may want to centralize the scoping: ```ruby class OnlyMyAccount < ActiveAdmin::AuthorizationAdapter @@ -101,7 +106,9 @@ class OnlyDashboard < ActiveAdmin::AuthorizationAdapter def authorized?(action, subject = nil) case subject when ActiveAdmin::Page - action == :read && subject.name == "Dashboard" && subject.namespace.name == :admin + action == :read && + subject.name == "Dashboard" && + subject.namespace.name == :admin else false end @@ -113,24 +120,23 @@ end By default Active Admin simplifies the controller actions into 4 actions: - * `:read` - This controls if the user can view the menu item as well as the - index and show screens. - * `:create` - This controls if the user can view the new screen and submit - the form to the create action. - * `:update` - This controls if the user can view the edit screen and submit - the form to the update action. - * `:destroy` - This controls if the user can delete a resource. +* `:read` - This controls if the user can view the menu item as well as the + index and show screens. +* `:create` - This controls if the user can view the new screen and submit + the form to the create action. +* `:update` - This controls if the user can view the edit screen and submit + the form to the update action. +* `:destroy` - This controls if the user can delete a resource. Each of these actions is available as a constant. Eg: `:read` is available as `ActiveAdmin::Authorization::READ`. - ## Checking for Authorization in Controllers and Views Active Admin provides a helper method to check if the current user is authorized to perform an action on a subject. -Simply use the `#authorized?(action, subject) method to check. +Use the `#authorized?(action, subject)` method to check. ```ruby ActiveAdmin.register Post do @@ -174,28 +180,28 @@ end Sub-classing `ActiveAdmin::AuthorizationAdapter` is fairly low level. Many times it's nicer to have a simpler DSL for managing authorization. Active Admin -provides an adapter out of the box for [CanCan](https://github.com/ryanb/cancan) -and [CanCanCan](https://github.com/CanCanCommunity/cancancan). +provides an adapter out of the box for [CanCanCan](https://github.com/CanCanCommunity/cancancan). -To use the CanCan adapter, simply update the configuration in the Active Admin +To use the CanCan adapter, update the configuration in the Active Admin initializer: ```ruby config.authorization_adapter = ActiveAdmin::CanCanAdapter ``` -You can also specify a method to be called on unauthorized access. This is necessary -in order to prevent a redirect loop that can happen if a user tries to access a page -they don't have permissions for (see [#2081](https://github.com/activeadmin/activeadmin/issues/2081)). +You can also specify a method to be called on unauthorized access. This is +necessary in order to prevent a redirect loop that can happen if a user tries to +access a page they don't have permissions for (see +[#2081](https://github.com/activeadmin/activeadmin/issues/2081)). ```ruby config.on_unauthorized_access = :access_denied ``` -The method `access_denied` would be defined in `application_controller.rb`. Here is one -example that redirects the user from the page they don't have permission to -access to a resource they have permission to access (organizations in this case), and -also displays the error message in the browser: +The method `access_denied` would be defined in `application_controller.rb`. Here +is one example that redirects the user from the page they don't have permission +to access to a resource they have permission to access (organizations in this +case), and also displays the error message in the browser: ```ruby class ApplicationController < ActionController::Base @@ -214,7 +220,7 @@ changed from the initializer: config.cancan_ability_class = "MyCustomAbility" ``` -Now you can simply use CanCan or CanCanCan the way that you would expect and +Now you can simply use CanCanCan the way that you would expect and Active Admin will use it for authorization: ```ruby @@ -226,23 +232,54 @@ class Ability can :manage, Post can :read, User can :manage, User, id: user.id - can :read, ActiveAdmin::Page, name: "Dashboard", namespace_name: :admin + can :read, ActiveAdmin::Page, name: "Dashboard", namespace_name: "admin" end end ``` -To view more details about the API's, visit project pages of [CanCan](https://github.com/ryanb/cancan) and [CanCanCan](https://github.com/CanCanCommunity/cancancan). +To view more details about the API's, visit project pages of +[CanCanCan](https://github.com/CanCanCommunity/cancancan). ## Using the Pundit Adapter -Active Admin provides an adapter out of the box also for [Pundit](https://github.com/elabs/pundit). +Active Admin also provides an adapter out of the box for +[Pundit](https://github.com/varvet/pundit). -To use the Pundit adapter, simply update the configuration in the Active Admin +To use the Pundit adapter, update the configuration in the Active Admin initializer: ```ruby config.authorization_adapter = ActiveAdmin::PunditAdapter ``` -You can simply use Pundit the way that you would expect and Active Admin will use it for authorization. Check Pundit's documentation to [set up Pundit in your application](https://github.com/elabs/pundit#installation). If you want to use batch actions just ensure that `destroy_all?` method is defined in your policy class. You can use this [template policy](https://github.com/activeadmin/activeadmin/blob/master/spec/support/templates/policies/application_policy.rb) in your application instead of default one generated by Pundit's `rails g pundit:install` command. +Once that's done, Active Admin will pick up your Pundit policies, and use +them for authorization. For more information about setting up Pundit, see +[their documentation](https://github.com/varvet/pundit#installation). + +Pundit also has [verify_authorized and/or verify_policy_scoped +methods](https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used) +to enforce usage of `authorized` and `policy_scope`. This conflicts with Active +Admin's authorization architecture, so if you're using those features, you'll +want to disable them for Active Admin's controllers: + +```ruby +class ApplicationController < ActionController::Base + include Pundit + after_action :verify_authorized, except: :index, unless: :active_admin_controller? + after_action :verify_policy_scoped, only: :index, unless: :active_admin_controller? + + def active_admin_controller? + is_a?(ActiveAdmin::BaseController) + end +end +``` + +If you want to use batch actions, ensure that `destroy_all?` method is defined +in your policy class. You can use this [template +policy](https://github.com/activeadmin/activeadmin/blob/master/spec/support/templates/policies/application_policy.rb) +in your application instead of default one generated by Pundit's +`rails g pundit:install` command. + +In addition, there are [example policies](https://github.com/activeadmin/activeadmin/tree/master/spec/support/templates/policies/active_admin) +for restricting access to ActiveAdmin's pages and comments. diff --git a/docs/14-gotchas.md b/docs/14-gotchas.md index f54d782d73b..efcff41cd41 100644 --- a/docs/14-gotchas.md +++ b/docs/14-gotchas.md @@ -1,39 +1,54 @@ -#Gotchas +--- +redirect_from: /docs/14-gotchas.html +--- + +# Gotchas + +## Security + +### Spreadsheet applications vulnerable to unescaped CSV data + +If your CSV export includes untrusted data provided by your users, it's possible +that they could include an executable formula that could call arbitrary commands +on your computer. See +[#4256](https://github.com/activeadmin/activeadmin/issues/4256) for more +details. ## Session Commits & Asset Pipeline -When configuring the asset pipeline ensure that the asset prefix -(`config.assets.prefix`) is not the same as the namespace of ActiveAdmin -(default namespace is `/admin`). If they are the same Sprockets will prevent the -session from being committed. Flash messages won't work and you will be unable to +When configuring the asset pipeline ensure that the asset prefix +(`config.assets.prefix`) is not the same as the namespace of ActiveAdmin +(default namespace is `/admin`). If they are the same Sprockets will prevent the +session from being committed. Flash messages won't work and you will be unable to use the session for storing anything. -For more information see the following post: -[http://www.intridea.com/blog/2013/3/20/rails-assets-prefix-may-disable-your-session](http://www.intridea.com/blog/2013/3/20/rails-assets-prefix-may-disable-your-session) +For more information see [the following +post](https://www.mobomo.com/2013/03/rails-assets-prefix-may-disable-your-session/). ## Helpers -There are two knowing gotchas with helpers. This hopefully will help you to +There are two known gotchas with helpers. This hopefully will help you to find a solution. ### Helpers are not reloading in development -This is a known and still open [issue](https://github.com/activeadmin/activeadmin/issues/697) -the only way is to restart your server each time you change a helper. +This is a known and still open +[issue](https://github.com/activeadmin/activeadmin/issues/697) the only way is +to restart your server each time you change a helper. ### Helper maybe not included by default -If you use `config.action_controller.include_all_helpers = false` in your application config, -you need to include it by hand. +If you use `config.action_controller.include_all_helpers = false` in your +application config, you need to include it by hand. #### Solutions -##### First use a monkey patch +##### First use an override -This works for all ActiveAdmin resources at once. +This works for all ActiveAdmin resources at once. Please [follow the Rails +guidelines for overriding](https://guides.rubyonrails.org/engines.html#improving-engine-functionality) this safely alongside Zeitwerk. ```ruby -# config/initializers/active_admin_helpers.rb ActiveAdmin::BaseController.class_eval do helper ApplicationHelper end @@ -53,23 +68,31 @@ end ## CSS -In order to avoid the override of your application style with the Active Admin one, you can do one of this things: -* You can properly move the generated file `active_admin.scss` from `app/assets/stylesheets` to `vendor/assets/stylesheets`. -* You can remove all `require_tree` comands from your root level css files, where the `active_admin.scss` is in the tree. +To avoid overriding your application styles with the ActiveAdmin styles, +remove the `require_tree` command from your application's CSS files, where +the `active_admin.scss` is in the tree. + +## Deprecation warnings with modern sass build tools + +Active Admin v3's SCSS is written for [sassc](https://rubygems.org/gems/sassc), which follows an older version of the SCSS specification. If you use a Node-based build system like esbuild, webpacker, or vite, you may encounter deprecation warnings for color functions like this when compiling assets: + +> DEPRECATION WARNING: lighten() is deprecated + +As a quick workaround, you may be able to silence these warnings by passing the `quietDeps` scss compilation option in your build system. With vite, follow these instructions: (note this requires installing the `sass-embedded` dependency). ## Conflicts ### With gems that provides a `search` class method on a model -If a gem defines a `search` class method on a model, this can result in conflicts +If a gem defines a `search` class method on a model, this can result in conflicts with the same method provided by `ransack` (a dependency of ActiveAdmin). -Each of this conflicts need to solved is a different way. Some solutions are +Each of this conflicts need to solved is a different way. Some solutions are listed below. #### `tire`, `retire` and `elasticsearch-rails` -This conflict can be solved, by using explicitly the `search` method of `tire`, +This conflict can be solved, by using explicitly the `search` method of `tire`, `retire` or `elasticsearch-rails`: ##### For `tire` and `retire` @@ -89,6 +112,14 @@ YourModel.__elasticsearch__.search ```ruby YourModel.solr_search ``` + ## Authentication & Application Controller -The `ActiveAdmin::BaseController` inherits from the `ApplicationController`. Any authentication method(s) specified in the `ApplicationController` callbacks will be called instead of the authentication method in the active admin config file. For example, if the ApplicationController has a callback `before_action :custom_authentication_method` and the config file's authentication method is `config.authentication_method = :authenticate_active_admin_user`, then `custom_authentication_method` will be called instead of `authenticate_active_admin_user`. \ No newline at end of file +The `ActiveAdmin::BaseController` inherits from the `ApplicationController`. Any +authentication method(s) specified in the `ApplicationController` callbacks will +be called instead of the authentication method in the active admin config file. +For example, if the ApplicationController has a callback `before_action +:custom_authentication_method` and the config file's authentication method is +`config.authentication_method = :authenticate_active_admin_user`, then +`custom_authentication_method` will be called instead of +`authenticate_active_admin_user`. diff --git a/docs/2-resource-customization.md b/docs/2-resource-customization.md index daa4a2d2374..2d2b51b1562 100644 --- a/docs/2-resource-customization.md +++ b/docs/2-resource-customization.md @@ -1,3 +1,7 @@ +--- +redirect_from: /docs/2-resource-customization.html +--- + # Working with Resources Every Active Admin resource corresponds to a Rails model. So before creating a @@ -6,18 +10,26 @@ resource you must first create a Rails model for it. ## Create a Resource The basic command for creating a resource is `rails g active_admin:resource Post`. -The generator will produce an empty `app/admin/post.rb` file like so: +The generator will produce a `app/admin/posts.rb` file like the following: ```ruby ActiveAdmin.register Post do - # everything happens here :D + permit_params :title + + filter :title + filter :created_at + filter :updated_at + + actions :all, except: [] + + # index, show, form ... end ``` -## Setting up Strong Parameters +The generator will try to determine possible fields for each section as best +as possible but you may need to tweak further to get started. -Rails 4 replaces `attr_accessible` with [Strong Parameters](https://github.com/rails/strong_parameters), -which moves attribute whitelisting from the model to the controller. +## Setting up Strong Parameters Use the `permit_params` method to define which attributes may be changed: @@ -27,8 +39,8 @@ ActiveAdmin.register Post do end ``` -Any form field that sends multiple values (such as a HABTM association, or an array attribute) -needs to pass an empty array to `permit_params`: +Any form field that sends multiple values (such as a HABTM association, or an +array attribute) needs to pass an empty array to `permit_params`: If your HABTM is `roles`, you should permit `role_ids: []` @@ -65,7 +77,17 @@ ActiveAdmin.register Post do end ``` -The `permit_params` call creates a method called `permitted_params`. You should use this method when overriding `create` or `update` actions: +If your resource is nested, declare `permit_params` after `belongs_to`: + +```ruby +ActiveAdmin.register Post do + belongs_to :user + permit_params :title, :content, :publisher_id +end +``` + +The `permit_params` call creates a method called `permitted_params`. You should +use this method when overriding `create` or `update` actions: ```ruby ActiveAdmin.register Post do @@ -94,6 +116,25 @@ ActiveAdmin.register Post do end ``` +## Renaming Action Items + +You can use translations to override labels and page titles for actions such as +new, edit, and destroy by providing a resource specific translation. For +example, to change 'New Offer' to 'Make an Offer' add the following in +config/locales/[en].yml: + +```yaml +en: + active_admin: + resources: + offer: # Registered resource + new_model: 'Make an Offer' # new action item + edit_model: 'Change Offer' # edit action item + delete_model: 'Cancel Offer' # delete action item +``` + +See the [default en.yml locale file](https://github.com/activeadmin/activeadmin/blob/master/config/locales/en.yml) for existing translations and examples. + ## Rename the Resource By default, any references to the resource (menu, routes, buttons, etc) in the @@ -133,7 +174,8 @@ The menu method accepts a hash with the following options: * `:label` - The string or proc label to display in the menu. If it's a proc, it will be called each time the menu is rendered. -* `:parent` - The string id (or label) of the parent used for this menu +* `:parent` - The string id (or label) of the parent used for this menu, or an array + of string ids (or labels) for a nested menu * `:if` - A block or a symbol of a method to call to decide if the menu item should be displayed * `:priority` - The integer value of the priority, which defaults to `10` @@ -158,20 +200,21 @@ end ### Menu Priority -Menu items are sorted first by their numeric priority, then alphabetically. Since -every menu by default has a priority of `10`, the menu is normally alphabetical. +Menu items are sorted first by their numeric priority, then alphabetically. Every +menu item has a default priority of `10`. -You can easily customize this: +You can customize this with: ```ruby ActiveAdmin.register Post do - menu priority: 1 # so it's on the very left + menu priority: 1 # so it's the first menu item visible end ``` ### Conditionally Showing / Hiding Menu Items Menu items can be shown or hidden at runtime using the `:if` option. + ```ruby ActiveAdmin.register Post do menu if: proc{ current_user.can_edit_posts? } @@ -195,6 +238,14 @@ end Note that the "Blog" parent menu item doesn't even have to exist yet; it can be dynamically generated for you. +To further nest an item under a submenu, provide an array of parents. + +```ruby +ActiveAdmin.register Post do + menu parent: ["Admin", "Blog"] +end +``` + ### Customizing Parent Menu Items All of the options given to a standard menu item are also available to @@ -246,9 +297,15 @@ config.namespace :admin do |admin| menu.add label: "The Application", url: "/", priority: 0 menu.add label: "Sites" do |sites| - sites.add label: "Google", url: "http://google.com", html_options: { target: :blank } - sites.add label: "Facebook", url: "http://facebook.com" - sites.add label: "Github", url: "http://github.com" + sites.add label: "Google", + url: "https://google.com", + html_options: { target: "_blank" } + + sites.add label: "Facebook", + url: "https://facebook.com" + + sites.add label: "Github", + url: "https://github.com" end end end @@ -287,7 +344,8 @@ end ## Eager loading -A common way to increase page performance is to elimate N+1 queries by eager loading associations: +A common way to increase page performance is to eliminate N+1 queries by eager +loading associations: ```ruby ActiveAdmin.register Post do @@ -297,7 +355,13 @@ end ## Customizing resource retrieval -If you need to customize the collection properties, you can overwrite the `scoped_collection` method. +Our controllers are built on [Inherited +Resources](https://github.com/activeadmin/inherited_resources), so you can use +[all of its +features](https://github.com/activeadmin/inherited_resources#overwriting-defaults). + +If you need to customize the collection properties, you can overwrite the +`scoped_collection` method. ```ruby ActiveAdmin.register Post do @@ -309,21 +373,33 @@ ActiveAdmin.register Post do end ``` -If you need to completely replace the record retrieving code (e.g., you have a custom -`to_param` implementation in your models), override the `resource` method on the controller: +If you need to completely replace the record retrieving code (e.g., you have a +custom `to_param` implementation in your models), override the `find_resource` method +on the controller: ```ruby ActiveAdmin.register Post do controller do def find_resource - Post.where(id: params[:id]).first! + scoped_collection.where(id: params[:id]).first! end end end ``` -Our controllers are built on [Inherited Resources](https://github.com/josevalim/inherited_resources), -so you can use [all of its features](https://github.com/josevalim/inherited_resources#overwriting-defaults). +Note that if you use an authorization library like CanCan, you should be careful +to not write code like this, otherwise **your authorization rules won't be +applied**: + +```ruby +ActiveAdmin.register Post do + controller do + def find_resource + Post.where(id: params[:id]).first! + end + end +end +``` ## Belongs To @@ -351,8 +427,8 @@ ActiveAdmin.register Project do sidebar "Project Details", only: [:show, :edit] do ul do - li link_to "Tickets", admin_project_tickets_path(project) - li link_to "Milestones", admin_project_milestones_path(project) + li link_to "Tickets", admin_project_tickets_path(resource) + li link_to "Milestones", admin_project_milestones_path(resource) end end end diff --git a/docs/3-index-pages.md b/docs/3-index-pages.md index 698ecc8df22..ff1a8edd3a6 100644 --- a/docs/3-index-pages.md +++ b/docs/3-index-pages.md @@ -1,3 +1,7 @@ +--- +redirect_from: /docs/3-index-pages.html +--- + # Customizing the Index Page Filtering and listing resources is one of the most important tasks for @@ -77,7 +81,8 @@ end Out of the box, Active Admin supports the following filter types: -* *:string* - A search field +* *:string* - A drop down for selecting "Contains", "Equals", "Starts with", + "Ends with" and an input for a value. * *:date_range* - A start and end date field with calendar inputs * *:numeric* - A drop down for selecting "Equal To", "Greater Than" or "Less Than" and an input for a value. @@ -100,6 +105,23 @@ the collection as a proc to be called at render time. filter :author, as: :check_boxes, collection: proc { Author.all } ``` +To override options for string or numeric filter pass `filters` option. + +```ruby + filter :title, filters: [:start, :end] +``` + +Also, if you don't need the select with the options 'cont', 'eq', 'start' or +'end' just add the option to the filter name with an underscore. + +For example: + +```ruby +filter :name_eq +# or +filter :name_cont +``` + You can change the filter label by passing a label option: ```ruby @@ -108,9 +130,11 @@ filter :author, label: 'Something else' By default, Active Admin will try to use ActiveModel I18n to determine the label. -You can also filter on more than one attribute of a model using the -[Ransack search predicate syntax](https://github.com/activerecord-hackery/ransack/wiki/Basic-Searching). If using a custom search method, you will -also need to specify the field type using `:as` and the label. +You can also filter on more than one attribute of a model using the [Ransack +search predicate +syntax](https://github.com/activerecord-hackery/ransack/wiki/Basic-Searching). +If using a custom search method, you will also need to specify the field type +using `:as` and the label. ```ruby filter :first_name_or_last_name_cont, as: :string, label: "Name" @@ -159,11 +183,19 @@ preserve_default_filters! remove_filter :id ``` +### Allow Filtering Attributes + +By default, filtering on any model attributes is denied, this is a security +feature to prevent users from filtering (reading by guessing) attributes that +shouldn't be accessible by them. +To allow filtering on attributes, follow the [Ransack Authorization guide] +to extend `ransackable_attributes` class method. + ## Index Scopes You can define custom scopes for your index page. This will add a tab bar above -the index table to quickly filter your collection on pre-defined scopes. There are -a number of ways to define your scopes: +the index table to quickly filter your collection on pre-defined scopes. There +are a number of ways to define your scopes: ```ruby scope :all, default: true @@ -181,11 +213,31 @@ scope ->{ Date.today.strftime '%A' }, :published_today scope("Inactive") { |scope| scope.where(active: false) } # conditionally show a custom controller scope -scope "Published", if: proc { current_admin_user.can? :manage, Posts } do |posts| +scope "Published", if: -> { current_admin_user.can? :manage, Posts } do |posts| posts.published end ``` +Scopes can be labelled with a translation, e.g. +`active_admin.scopes.scope_method`. + +### Scopes groups + +You can assign group names to scopes to keep related scopes together and separate them from the rest. + +```ruby +# a scope in the default group +scope :all + +# two scopes used to filter by status +scope :active, group: :status +scope :inactive, group: :status + +# two scopes used to filter by date +scope :today, group: :date +scope :tomorrow, group: :date +``` + ## Index default sort order You can define the default sort order for index pages: @@ -214,11 +266,19 @@ ActiveAdmin.register Post do end ``` +Or allow users to choose themselves using dropdown with values + +```ruby +ActiveAdmin.register Post do + config.per_page = [10, 50, 100] +end +``` + You can change it per request / action too: ```ruby controller do - before_filter :only => :index do + before_action only: :index do @per_page = 100 end end @@ -267,6 +327,11 @@ ActiveAdmin.setup do |config| end ``` -Note: you have to actually implement PDF rendering for your action, ActiveAdmin does not provide this feature. This setting just allows you to specify formats that you want to show up under the index collection. +Note: you have to actually implement PDF rendering for your action, ActiveAdmin +does not provide this feature. This setting just allows you to specify formats +that you want to show up under the index collection. + +You'll need to use a PDF rendering library like PDFKit or WickedPDF to get the +PDF generation you want. -You'll need to use a PDF rendering library like PDFKit or WickedPDF to get the PDF generation you want. +[Ransack Authorization guide]: https://activerecord-hackery.github.io/ransack/going-further/other-notes/#authorization-allowlistingdenylisting diff --git a/docs/3-index-pages/custom-index.md b/docs/3-index-pages/custom-index.md index d868701f6f5..bdd0e666250 100644 --- a/docs/3-index-pages/custom-index.md +++ b/docs/3-index-pages/custom-index.md @@ -1,3 +1,7 @@ +--- +redirect_from: /docs/3-index-pages/custom-index.html +--- + # Custom Index If the supplied Active Admin index components are insufficient for your project @@ -26,6 +30,6 @@ The build method takes a PagePresenter object and collection of whatever you choose. The `index_name` class method takes no arguments and returns a string that should -be representative of the the class name. If this method is not defined, your +be representative of the class name. If this method is not defined, your index component will not be able take advantage of Active Admin's *multiple index pages* feature. diff --git a/docs/3-index-pages/index-as-block.md b/docs/3-index-pages/index-as-block.md index b74de5f162e..db5c32c1252 100644 --- a/docs/3-index-pages/index-as-block.md +++ b/docs/3-index-pages/index-as-block.md @@ -1,8 +1,6 @@ - +--- +redirect_from: /docs/3-index-pages/index-as-block.html +--- # Index as a Block diff --git a/docs/3-index-pages/index-as-blog.md b/docs/3-index-pages/index-as-blog.md index 184b664658a..212646c578e 100644 --- a/docs/3-index-pages/index-as-blog.md +++ b/docs/3-index-pages/index-as-blog.md @@ -1,8 +1,6 @@ - +--- +redirect_from: /docs/3-index-pages/index-as-blog.html +--- # Index as Blog @@ -30,7 +28,7 @@ end ``` 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 +used as the contents of the title. The resource being rendered is passed in to the block. For Example: ```ruby diff --git a/docs/3-index-pages/index-as-grid.md b/docs/3-index-pages/index-as-grid.md index a31435991ec..c4e5f92ae73 100644 --- a/docs/3-index-pages/index-as-grid.md +++ b/docs/3-index-pages/index-as-grid.md @@ -1,8 +1,6 @@ - +--- +redirect_from: /docs/3-index-pages/index-as-grid.html +--- # Index as a Grid diff --git a/docs/3-index-pages/index-as-table.md b/docs/3-index-pages/index-as-table.md index 676cee8535f..99b84a82b01 100644 --- a/docs/3-index-pages/index-as-table.md +++ b/docs/3-index-pages/index-as-table.md @@ -1,8 +1,6 @@ - +--- +redirect_from: /docs/3-index-pages/index-as-table.html +--- # Index as a Table @@ -74,7 +72,7 @@ index do selectable_column column :title actions do |post| - item "Preview", admin_preview_post_path(post), class: "member_link" + item "Preview", admin_preview_post_path(post), class: "preview-link" end end ``` @@ -86,8 +84,6 @@ index do column :title actions defaults: false do |post| item "View", admin_post_path(post) - item "Edit", edit_admin_post_path(post) - item "Delete", admin_post_path(post), method: :delete end end ``` @@ -103,39 +99,13 @@ index do end ``` -In case you prefer to list actions links in a dropdown menu: - -```ruby -index do - selectable_column - column :title - actions dropdown: true do |post| - item "Preview", admin_preview_post_path(post) - end -end -``` - -In addition, you can insert the position of the row in the greater collection by using the index_column special command: - -```ruby -index do - selectable_column - index_column - column :title -end -``` - -index_column take an optional offset parameter to allow a developer to set the starting number for the index (default is 1). - ## 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. @@ -164,6 +134,24 @@ index do 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 +``` + ## Associated Sorting You're normally able to sort columns alphabetically, but by default you @@ -179,6 +167,13 @@ controller do 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 @@ -201,13 +196,22 @@ index do end ``` -## Custom row class +## 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 special class to table rows pass the proc object as a `:row_class` option -of the `index` method. +In order to add HTML attributes to table rows, use a proc object in the `:row_html` option. ```ruby -index row_class: ->elem { 'active' if elem.active? } do +index row_html: ->elem { { class: ('active' if elem.active?), data: { 'element-id' => elem.id } } } do # columns end ``` diff --git a/docs/4-csv-format.md b/docs/4-csv-format.md index 2a03740dc14..d72503eeffa 100644 --- a/docs/4-csv-format.md +++ b/docs/4-csv-format.md @@ -1,3 +1,7 @@ +--- +redirect_from: /docs/4-csv-format.html +--- + # Customizing the CSV format Active Admin provides CSV file downloads on the index screen for each Resource. @@ -11,7 +15,7 @@ ActiveAdmin.register Post do csv do column :title column(:author) { |post| post.author.full_name } - column('bODY', humanize_name: false) # preserve case + column('body', humanize_name: false) # preserves case of column title end end ``` @@ -38,3 +42,33 @@ config.csv_options = { col_sep: ';' } # Force the use of quotes config.csv_options = { force_quotes: true } ``` + +You can customize the filename by overriding `csv_filename` in the controller block. + +```ruby +ActiveAdmin.register User do + controller do + def csv_filename + 'User Details.csv' + end + end +end +``` + +## Streaming + +By default Active Admin streams the CSV response to your browser as it's generated. +This is good because it prevents request timeouts, for example the infamous H12 +error on Heroku. + +However if an exception occurs while generating the CSV, the request will eventually +time out, with the last line containing the exception message. CSV streaming is +disabled in development to help debug these exceptions. That lets you use tools like +better_errors and web-console to debug the issue. If you want to customize the +environments where CSV streaming is disabled, you can change this setting: + +```ruby +# config/initializers/active_admin.rb + +config.disable_streaming_in = ['development', 'staging'] +``` diff --git a/docs/5-forms.md b/docs/5-forms.md index 0165afb53ee..19f3ddb0325 100644 --- a/docs/5-forms.md +++ b/docs/5-forms.md @@ -1,3 +1,7 @@ +--- +redirect_from: /docs/5-forms.html +--- + # Forms Active Admin gives you complete control over the output of the form by creating @@ -6,7 +10,7 @@ a thin DSL on top of [Formtastic](https://github.com/justinfrench/formtastic): ```ruby ActiveAdmin.register Post do - form do |f| + form title: 'A custom title' do |f| inputs 'Details' do input :title input :published_at, label: "Publish Post At" @@ -24,7 +28,7 @@ ActiveAdmin.register Post do end ``` -For more details, please see Formtastic's documentation. +For more details, please see [Formtastic's documentation](https://github.com/justinfrench/formtastic/wiki). ## Default @@ -52,7 +56,7 @@ Which looks for something like this: ```ruby # app/views/admin/posts/_form.html.arb -insert_tag active_admin_form_for resource do |f| +active_admin_form_for [:admin, resource] do |f| inputs :title, :body actions end @@ -60,12 +64,32 @@ end This is a regular Rails partial so any template engine may be used. +You can also use the `ActiveAdmin::FormBuilder` as builder in your Formtastic +Form for use the same helpers are used in the admin file: + +```ruby + = semantic_form_for [:admin, @post], builder: ActiveAdmin::FormBuilder do |f| + = f.inputs "Details" do + = f.input :title + - f.has_many :taggings, sortable: :position, sortable_start: 1 do |t| + - t.input :tag + = f.actions + +``` + ## Nested Resources -You can create forms with nested models using the `has_many` method, even if your model uses `has_one`: +You can create forms with nested models using the `has_many` method, even if +your model uses `has_one`: ```ruby ActiveAdmin.register Post do + permit_params :title, + :published_at, + :body, + categories_attributes: [:id, :title, :_destroy], + taggings_attributes: [:id, :tag], + comment_attributes: [:id, :body, :_destroy] form do |f| f.inputs 'Details' do @@ -73,18 +97,22 @@ ActiveAdmin.register Post do f.input :published_at, label: 'Publish Post At' end f.inputs 'Content', :body - f.inputs do - f.has_many :categories, heading: 'Themes', allow_destroy: true, new_record: false do |a| + f.inputs 'Themes' do + f.has_many :categories, heading: false, allow_destroy: true, new_record: false do |a| a.input :title end end - f.inputs do - f.has_many :taggings, sortable: :position, sortable_start: 1 do |t| + f.inputs 'Tags' do + f.has_many :taggings, heading: false, sortable: :position, sortable_start: 1 do |t| t.input :tag end end - f.inputs do - f.has_many :comment, new_record: 'Leave Comment' do |b| + f.inputs 'Comments' do + f.has_many :comments, + heading: false, + new_record: 'Leave Comment', + remove_record: 'Remove Comment', + allow_destroy: -> (c) { c.author?(current_admin_user) } do |b| b.input :body end end @@ -94,66 +122,89 @@ ActiveAdmin.register Post do end ``` +*NOTE*: In addition to using `has_many` as illustrated above, you'll need to add +`accepts_nested_attributes` to your parent model and [configure strong parameters](https://activeadmin.info/2-resource-customization.html) + The `:allow_destroy` option adds a checkbox to the end of the nested form allowing removal of the child object upon submission. Be sure to set `allow_destroy: true` -on the association to use this option. +on the association to use this option. It is possible to associate +`:allow_destroy` with a string or a symbol, corresponding to the name of a child +object's method that will get called, or with a Proc object. The Proc object +receives the child object as a parameter and should return either true or false. + +The `:heading` option adds a custom heading. You can hide it entirely by passing +`false`. -The `:heading` option adds a custom heading. You can hide it entirely by passing `false`. +The `:new_record` option controls the visibility of the new record button (shown +by default). If you pass a string, it will be used as the text for the new +record button. -The `:new_record` option controls the visibility of the new record button (shown by default). -If you pass a string, it will be used as the text for the new record button. +The `:remove_record` option controls the text of the remove button (shown after +the new record button is pressed). If you pass a string, it will be used as the +text for the remove button. -The `:sortable` option adds a hidden field and will enable drag & drop sorting of the children. It -expects the name of the column that will store the index of each child. +The `:sortable` option adds a hidden field and will enable drag & drop sorting +of the children. It expects the name of the column that will store the index of +each child. -The `:sortable_start` option sets the value (0 by default) of the first position in the list. +The `:sortable_start` option sets the value (0 by default) of the first position +in the list. ## Datepicker -ActiveAdmin offers the `datepicker` input, which uses the [jQuery UI datepicker](http://jqueryui.com/datepicker/). -The datepicker input accepts any of the options available to the standard jQueryUI Datepicker. For example: +ActiveAdmin offers the `datepicker` input, which uses the [jQuery UI +datepicker](https://jqueryui.com/datepicker/). The datepicker input accepts any +of the options available to the standard jQueryUI Datepicker. For example: ```ruby form do |f| - f.input :starts_at, as: :datepicker, datepicker_options: { min_date: "2013-10-8", max_date: "+3D" } - f.input :ends_at, as: :datepicker, datepicker_options: { min_date: 3.days.ago.to_date, max_date: "+1W +5D" } + f.input :starts_at, as: :datepicker, + datepicker_options: { + min_date: "2013-10-8", + max_date: "+3D" + } + + f.input :ends_at, as: :datepicker, + datepicker_options: { + min_date: 3.days.ago.to_date, + max_date: "+1W +5D" + } end ``` +Datepicker also accepts the `:label` option as a string or proc to display. +If it's a proc, it will be called each time the datepicker is rendered. + ## Displaying Errors To display a list of all validation errors: ```ruby form do |f| - f.semantic_errors *f.object.errors.keys + f.semantic_errors *f.object.errors.attribute_names + # ... end ``` This is particularly useful to display errors on virtual or hidden attributes. -# Tabs +# Customize the Create Another checkbox -You can arrage content in tabs as shown below: +In order to simplify creating multiple resources you may enable ActiveAdmin to +show nice "Create Another" checkbox alongside of Create Model button. It may be +enabled for the whole application: ```ruby - form do |f| - tabs do - tab 'Basic' do - f.inputs 'Basic Details' do - f.input :email - f.input :password - f.input :password_confirmation - end - end +ActiveAdmin.setup do |config| + config.create_another = true +end +``` - tab 'Advanced' do - f.inputs 'Advanced Details' do - f.input :role - end - end - end - f.actions - end +or for the particular resource: + +```ruby +ActiveAdmin.register Post do + config.create_another = true +end ``` diff --git a/docs/6-show-pages.md b/docs/6-show-pages.md index 0f9c421ea87..e5f3bd1256d 100644 --- a/docs/6-show-pages.md +++ b/docs/6-show-pages.md @@ -1,6 +1,10 @@ +--- +redirect_from: /docs/6-show-pages.html +--- # Customize the Show Page -The show block is rendered within the context of the view and uses [Arbre](https://github.com/activeadmin/arbre) syntax. +The show block is rendered within the context of the view and uses +[Arbre](https://github.com/activeadmin/arbre) syntax. With the `show` block, you can render anything you want. @@ -26,18 +30,18 @@ ActiveAdmin.register Post do end ``` -If you'd like to keep the default AA look, you can use `attributes_table`: +If you'd like to keep the default AA look, you can use `attributes_table_for`: ```ruby ActiveAdmin.register Ad do show do - attributes_table do + attributes_table_for(resource) do row :title row :image do |ad| image_tag ad.image.url end end - active_admin_comments + active_admin_comments_for(resource) end end ``` @@ -62,10 +66,10 @@ ActiveAdmin.register Book do column :page end end - active_admin_comments + active_admin_comments_for(resource) end - sidebar "Details", only: :show do + sidebar :details, only: :show do attributes_table_for book do row :title row :author @@ -76,30 +80,12 @@ ActiveAdmin.register Book do end ``` -# Tabs - -You can arrage content in tabs as shown below: +If you want to keep the default show contents, but add something else around it: ```ruby - ActiveAdmin.register Order do - show do - tabs do - tab 'Overview' do - attributes_table do - row(:status) { status_tag(order.status) } - row(:paid) { number_to_currency(order.amount_paid_in_dollars) } - end - end - - tab 'Payments' do - table_for order.payments do - column('Payment Type') { |p| p.payment_type.titleize } - column('Received On', :created_at) - column('Payment Details & Notes', :notes) - column('Amount') { |p| number_to_currency(p.amount_in_dollars) } - end - end - end - end - end +show do + default_main_content + h3 "Other Details" + # ... +end ``` diff --git a/docs/7-sidebars.md b/docs/7-sidebars.md index b8089b174e0..4c5e5e000a0 100644 --- a/docs/7-sidebars.md +++ b/docs/7-sidebars.md @@ -1,10 +1,13 @@ +--- +redirect_from: /docs/7-sidebars.html +--- # Sidebar Sections Sidebars allow you to put whatever content you want on the side the page. ```ruby sidebar :help do - "Need help? Email us at help@example.com" + para "Need help? Email us at help@example.com" end ``` @@ -26,7 +29,7 @@ Sidebars can be rendered on a specific action by passing `:only` or `:except`. ```ruby sidebar :help, only: :index do - "Need help? Email us at help@example.com" + para "Need help? Email us at help@example.com" end ``` @@ -35,7 +38,7 @@ pass it a proc which will be rendered within the view context. ```ruby sidebar :help, if: proc{ current_admin_user.super_admin? } do - "Only for super admins!" + span "Only for super admins!" end ``` @@ -65,7 +68,8 @@ By default sidebars are positioned in the same order as they defined, but it's a possible to specify their position manually: ```ruby -sidebar :help, priority: 0 # will push Help section to the top (above default Filters section) +# will push Help section to the top (above default Filters section) +sidebar :help, priority: 0 ``` Default sidebar priority is `10`. diff --git a/docs/8-custom-actions.md b/docs/8-custom-actions.md index 92cb3e9cc15..6ec52faf91b 100644 --- a/docs/8-custom-actions.md +++ b/docs/8-custom-actions.md @@ -1,3 +1,7 @@ +--- +redirect_from: /docs/8-custom-actions.html +--- + # Custom Controller Actions Active Admin allows you to override and modify the underlying controller which @@ -58,7 +62,7 @@ HTTP verbs. In that case, this is the suggested approach: ```ruby member_action :foo, method: [:get, :post] do if request.post? - resource.update_attributes! foo: params[:foo] || {} + resource.update! foo: params[:foo] || {} head :ok else render :foo @@ -128,18 +132,31 @@ post: ```ruby action_item :view, only: :show do - link_to 'View on site', post_path(post) if post.published? + link_to 'View on site', post_path(resource) if resource.published? end ``` Actions items also accept the `:if` option to conditionally display them: ```ruby -action_item :super_action, only: :show, if: proc{ current_admin_user.super_admin? } do +action_item :super_action, + only: :show, + if: proc{ current_admin_user.super_admin? } do "Only display this to super admins on the show screen" end ``` +By default action items are positioned in the same order as they defined (after default actions), +but it’s also possible to specify their position manually: + +```ruby +action_item :help, priority: 0 do + "Display this action to the first position" +end +``` + +Default action item priority is 10. + # Modifying the Controller The generated controller is available to you within the registration block by diff --git a/docs/9-batch-actions.md b/docs/9-batch-actions.md index 1b5f52e99ed..3331ad8008e 100644 --- a/docs/9-batch-actions.md +++ b/docs/9-batch-actions.md @@ -1,3 +1,7 @@ +--- +redirect_from: /docs/9-batch-actions.html +--- + # Batch Actions By default, the index page provides you a "Batch Action" to quickly delete records, @@ -48,7 +52,7 @@ end # app/admin/post.rb ActiveAdmin.register Post do - + # Resource level: config.batch_actions = false end @@ -144,19 +148,19 @@ end When you have dynamic form inputs you can pass a proc instead: ```ruby -# NOTE: multi-pluck is new to Rails 4 -batch_action :doit, form: ->{{user: User.pluck(:name, :id)}} do |ids, inputs| +batch_action :doit, form: -> { {user: User.pluck(:name, :id)} } do |ids, inputs| User.find(inputs[:user]) # ... end ``` -Under the covers this is powered by the JS `ActiveAdmin.modal_dialog` which you can use yourself: +Under the covers this is powered by the JS `ActiveAdmin.ModalDialog` which you +can use yourself: ```coffee if $('body.admin_users').length $('a[data-prompt]').click -> - ActiveAdmin.modal_dialog $(@).data('prompt'), comment: 'textarea', + ActiveAdmin.ModalDialog $(@).data('prompt'), comment: 'textarea', (inputs)=> $.post "/admin/users/#{$(@).data 'id'}/change_state", comment: inputs.comment, state: $(@).data('state'), @@ -190,38 +194,23 @@ en: publish: "Publish" ``` -### Support for other index types +### Support for custom index views -You can easily use `batch_action` in the other index views, *Grid*, *Block*, -and *Blog*; however, these will require custom styling to fit your needs. +You can use `batch_action` in a custom index view, however, these will require custom styling to fit your needs. ```ruby ActiveAdmin.register Post do - # By default, the "Delete" batch action is provided - - # Index as Grid - index as: :grid do |post| + index as: :custom do |post| resource_selection_cell post h2 auto_link post end - - # Index as Blog requires nothing special - - # Index as Block - index as: :block do |post| - div for: post do - resource_selection_cell post - end - end - -end ``` -### BTW +### Note on implementation -In order to perform the batch action, the entire *Table*, *Grid*, etc. is -wrapped in a form that submits the IDs of the selected rows to your batch_action. +In order to perform the batch action, the entire index view is +wrapped in a form that submits the IDs of the selected rows to your `batch_action`. Since nested `
` tags in HTML often results in unexpected behavior, you may need to modify the custom behavior you've built using to prevent conflicts. @@ -229,5 +218,5 @@ may need to modify the custom behavior you've built using to prevent conflicts. Specifically, if you are using HTTP methods like `PUT` or `PATCH` with a custom form on your index page this may result in your batch action being `PUT`ed instead of `POST`ed which will create a routing error. You can get around this -by either moving the nested form to another page or using a POST so it doesn't +by either moving the nested form to another page or using a `POST` so it doesn't override the batch action. As well, behavior may vary by browser. diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 00000000000..8413804eb41 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +activeadmin.info diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index ba065644bf5..00000000000 --- a/docs/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# ActiveAdmin Documentation - -## Content - -- [Installation](0-installation.md) -- [General Configuration](1-general-configuration.md) -- [Resource Customization](2-resource-customization.md) -- [Index Pages](3-index-pages.md) - - [Custom Index](3-index-pages/custom-index.md) - - [Index as Table](3-index-pages/index-as-table.md) - - [Index as Grid](3-index-pages/index-as-grid.md) - - [Index as Blocks](3-index-pages/index-as-block.md) - - [Index as Blog](3-index-pages/index-as-blog.md) -- [Csv Format](4-csv-format.md) -- [Forms](5-forms.md) -- [Show Pages](6-show-pages.md) -- [Sidebars](7-sidebars.md) -- [Custom Actions](8-custom-actions.md) -- [Batch Actions](9-batch-actions.md) -- [Custom Pages](10-custom-pages.md) -- [Decorators](11-decorators.md) -- [Arbre Components](12-arbre-components.md) -- [Authorization Adapter](13-authorization-adapter.md) -- [Gotchas](14-gotchas.md) diff --git a/docs/documentation.md b/docs/documentation.md new file mode 100644 index 00000000000..8b0f0726b87 --- /dev/null +++ b/docs/documentation.md @@ -0,0 +1,8 @@ +--- +head: + - - meta + - http-equiv: refresh + content: 0; url=/ +--- + +This page has been removed. You will be redirected to [the homepage](/). diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000000..dd8b02ed0d0 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,49 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: ActiveAdmin + text: An admin engine for Rails applications + tagline: Abstracts common patterns to implement beautiful and elegant interfaces with ease. + + actions: + - theme: brand + text: Getting Started + link: /0-installation + - theme: alt + text: View on GitHub + link: https://github.com/activeadmin/activeadmin + +features: + - icon: 🌎 + title: Global Navigation + details: Customizable navigation allows you to create usable admin interfaces for your business. + - icon: 🔒 + title: User Authentication + details: Use the bundled Devise configuration or implement your own authorization using the provided hooks. + - icon: 🎬 + title: Action Items + details: Add buttons or links as action items in the page header for a resource. + link: /8-custom-actions + linkText: Learn about action items + - icon: 🔍 + title: Filters + details: Allow users to filter resources by searching strings, text fields, dates, and numeric values. + link: /3-index-pages + linkText: Learn about filters + - icon: 🗂️ + title: Scopes + details: Use scopes to create sections of mutually exclusive resources for quick navigation and reporting. + - icon: 📑 + title: Custom Index Views + details: The default index screen is a table view, but custom index views are supported. + - icon: 📋 + title: Sidebar Sections + details: Add your own sections to the sidebar using a simple DSL. + link: /7-sidebars + linkText: Learn about sidebar sections + - icon: 💾 + title: Downloads + details: Each resource becomes available as CSV, JSON, and XML with customizable output. +--- diff --git a/docs/markdown-examples.md b/docs/markdown-examples.md new file mode 100644 index 00000000000..d715a2bf94a --- /dev/null +++ b/docs/markdown-examples.md @@ -0,0 +1,127 @@ +--- +outline: deep +--- + +# Markdown Extension Examples + +This page demonstrates some of the built-in markdown extensions provided by VitePress. + +## Syntax Highlighting + +VitePress provides Syntax Highlighting powered by [Shikiji](https://github.com/antfu/shikiji), with additional features like line-highlighting: + +**Input** + +````md +```js{4} +export default { + data () { + return { + msg: 'Highlighted!' + } + } +} +``` +```` + +**Output** + +```js{4} +export default { + data () { + return { + msg: 'Highlighted!' + } + } +} +``` + +## Custom Containers + +**Input** + +```md +::: info +This is an info box. +::: + +::: tip +This is a tip. +::: + +::: warning +This is a warning. +::: + +::: danger +This is a dangerous warning. +::: + +::: details +This is a details block. +::: +``` + +**Output** + +::: info +This is an info box. +::: + +::: tip +This is a tip. +::: + +::: warning +This is a warning. +::: + +::: danger +This is a dangerous warning. +::: + +::: details +This is a details block. +::: + +## Runtime API Examples + +This page demonstrates usage of some of the runtime APIs provided by VitePress. + +The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files: + +```md + + +### Theme Data +
{{ theme }}
+ +### Page Data +
{{ page }}
+ +### Page Frontmatter +
{{ frontmatter }}
+``` + + + +### Theme Data +
{{ theme }}
+ +### Page Data +
{{ page }}
+ +### Page Frontmatter +
{{ frontmatter }}
+ +## More + +Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown) and the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000000..ba1c6908d1e --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,34 @@ +import js from "@eslint/js"; + +export default [ + { + // as the sole object key, this ignores globally + ignores: [ + "app/assets/**", + "coverage/**", + "docs/.vitepress/dist/**", + "lib/generators/**", + "src/**", + "tmp/**", + "vendor/**", + "plugin.js" + ], + }, + { + ...js.configs.recommended, + languageOptions: { + globals: { + document: "readonly", + FormData: "readonly", + localStorage: "readonly", + URLSearchParams: "readonly", + window: "readonly", + } + }, + }, + { + rules: { + "no-unused-vars": ["error", { "argsIgnorePattern": "event" }], + } + }, +]; diff --git a/features/action_item.feature b/features/action_item.feature index 17d5ed37b59..0be8f353737 100644 --- a/features/action_item.feature +++ b/features/action_item.feature @@ -25,14 +25,14 @@ Feature: Action Item Then I should see an action item link to "Embiggen" When I am on the index page for posts - When I follow "New Post" + And I follow "New Post" Then I should see an action item link to "Embiggen" Scenario: Create an member action with if clause that returns true Given a configuration of: """ ActiveAdmin.register Post do - action_item :embiggen, :if => proc{ !current_active_admin_user.nil? } do + action_item :embiggen, if: proc{ !current_active_admin_user.nil? } do link_to "Embiggen", '/' end end @@ -47,14 +47,14 @@ Feature: Action Item Then I should see an action item link to "Embiggen" When I am on the index page for posts - When I follow "New Post" + And I follow "New Post" Then I should see an action item link to "Embiggen" Scenario: Create an member action with if clause that returns false Given a configuration of: """ ActiveAdmin.register Post do - action_item :embiggen, :if => proc{ current_active_admin_user.nil? } do + action_item :embiggen, if: proc{ current_active_admin_user.nil? } do link_to "Embiggen", '/' end end @@ -69,5 +69,5 @@ Feature: Action Item Then I should not see an action item link to "Embiggen" When I am on the index page for posts - When I follow "New Post" + And I follow "New Post" Then I should not see an action item link to "Embiggen" diff --git a/features/authorization.feature b/features/authorization.feature index 975f8db0909..5d3f5375235 100644 --- a/features/authorization.feature +++ b/features/authorization.feature @@ -1,3 +1,4 @@ +@authorization Feature: Authorizing Access Ensure that access denied exceptions are managed @@ -14,7 +15,7 @@ Feature: Authorizing Access when normalized(Post) case action - when ActiveAdmin::Auth::UPDATE, ActiveAdmin::Auth::DESTROY + when :edit, :update, :destroy false else true @@ -44,7 +45,6 @@ Feature: Authorizing Access """ And I am on the index page for posts - @allow-rescue Scenario: Attempt to access a resource I am not authorized to see When I go to the last post's edit page Then I should see "You are not authorized to perform this action" @@ -53,12 +53,10 @@ Feature: Authorizing Access When I follow "View" Then I should not see an action item link to "Edit" - @allow-rescue Scenario: Attempting to visit a Page without authorization When I go to the admin no access page Then I should see "You are not authorized to perform this action" - @allow-rescue Scenario: Viewing a page with authorization When I go to the admin dashboard page Then I should see "Dashboard" diff --git a/features/authorization_cancan.feature b/features/authorization_cancan.feature index 30f87ef6f0a..4fa6880c02a 100644 --- a/features/authorization_cancan.feature +++ b/features/authorization_cancan.feature @@ -1,3 +1,4 @@ +@authorization Feature: Authorizing Access using CanCan Background: @@ -12,12 +13,12 @@ Feature: Authorizing Access using CanCan def initialize(user) # Manage Posts - can [:edit, :destroy], Post, :author_id => user.id + can [:edit, :destroy], Post, author_id: user.id can :read, Post # View Pages - can :read, ActiveAdmin::Page, :name => "Dashboard" - cannot :read, ActiveAdmin::Page, :name => "No Access" + can :read, ActiveAdmin::Page, name: "Dashboard" + cannot :read, ActiveAdmin::Page, name: "No Access" end end @@ -32,7 +33,6 @@ Feature: Authorizing Access using CanCan """ And I am on the index page for posts - @allow-rescue Scenario: Attempt to access a resource I am not authorized to see When I go to the last post's edit page Then I should see "You are not authorized to perform this action" @@ -41,12 +41,10 @@ Feature: Authorizing Access using CanCan When I follow "View" Then I should not see an action item link to "Edit" - @allow-rescue Scenario: Attempting to visit a Page without authorization When I go to the admin no access page Then I should see "You are not authorized to perform this action" - @allow-rescue Scenario: Viewing a page with authorization When I go to the admin dashboard page Then I should see "Dashboard" diff --git a/features/authorization_pundit.feature b/features/authorization_pundit.feature index 8b48e670822..8fed166e92a 100644 --- a/features/authorization_pundit.feature +++ b/features/authorization_pundit.feature @@ -1,3 +1,4 @@ +@authorization Feature: Authorizing Access using Pundit Background: @@ -17,7 +18,6 @@ Feature: Authorizing Access using Pundit """ And I am on the index page for posts - @allow-rescue Scenario: Attempt to access a resource I am not authorized to see When I go to the last post's edit page Then I should see "You are not authorized to perform this action" @@ -26,12 +26,21 @@ Feature: Authorizing Access using Pundit When I follow "View" Then I should not see an action item link to "Edit" - @allow-rescue Scenario: Attempting to visit a Page without authorization When I go to the admin no access page Then I should see "You are not authorized to perform this action" - @allow-rescue Scenario: Viewing a page with authorization When I go to the admin dashboard page Then I should see "Dashboard" + + Scenario: Comment policy allows access to my own comments only + Given 5 comments added by admin with an email "commenter@example.com" + And 3 comments added by admin with an email "admin@example.com" + When I am on the dashboard + Then I should see a menu item for "Comments" + When I go to the index page for comments + Then I should see 3 Comments in the table + When I go to the last post's show page + Then I should see 3 comments + And I should be able to add a comment diff --git a/features/belongs_to.feature b/features/belongs_to.feature index fb48034096c..790bd8a1ce8 100644 --- a/features/belongs_to.feature +++ b/features/belongs_to.feature @@ -16,13 +16,109 @@ Feature: Belongs To end """ When I go to the last author's posts - Then the "Posts" tab should be selected - And I should not see a menu item for "Users" - And I should see "Displaying 1 Post" + Then the "Users" menu item should be selected + And I should not see a menu item for "Posts" + And I should see "Showing 1 of 1" And I should see a link to "Users" in the breadcrumb And I should see a link to "Jane Doe" in the breadcrumb - When I follow "Edit" - Then I should see a link to "Hello World" in the breadcrumb + + Scenario: Updating a child resource page + Given a configuration of: + """ + ActiveAdmin.register User + ActiveAdmin.register Post do + belongs_to :user + permit_params :title, :body, :published_date + + form do |f| + f.inputs "Your Post" do + f.input :title + f.input :body + end + f.inputs "Publishing" do + f.input :published_date + end + f.actions + end + end + """ + When I go to the last author's last post page + And I follow "Edit Post" + Then I should see the element "form[action='/admin/users/2/posts/2']" + And I should see a link to "Hello World" in the breadcrumb + + When I press "Update Post" + Then I should see "Post was successfully updated." + + Scenario: Updating a child resource page with custom configuration + Given a configuration of: + """ + ActiveAdmin.register User + ActiveAdmin.register Post do + belongs_to :author, class_name: "User", param: "user_id", route_name: "user" + permit_params :title + + form do |f| + f.actions + end + end + """ + When I go to the last author's last post page + And I follow "Edit Post" + Then I should see the element "form[action='/admin/users/2/posts/2']" + And I should see a link to "Hello World" in the breadcrumb + + When I press "Update Post" + Then I should see "Post was successfully updated." + + Scenario: Creating a child resource page + Given a configuration of: + """ + ActiveAdmin.register User + ActiveAdmin.register Post do + belongs_to :user + permit_params :title, :body, :published_date + + form do |f| + f.inputs "Your Post" do + f.input :title + f.input :body + end + f.inputs "Publishing" do + f.input :published_date + end + f.actions + end + end + """ + When I go to the last author's posts + And I follow "New Post" + Then I should see the element "form[action='/admin/users/2/posts']" + When 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." + 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 "Author" with "Jane Doe" + + Scenario: Creating a child resource page when belongs to defined after permitted params + Given a configuration of: + """ + ActiveAdmin.register User + ActiveAdmin.register Post do + permit_params :title, :body, :published_date + belongs_to :user + + form do |f| + f.actions + end + end + """ + When I go to the last author's posts + And I follow "New Post" + Then I should see the element "form[action='/admin/users/2/posts']" Scenario: Viewing a child resource page Given a configuration of: @@ -35,22 +131,22 @@ Feature: Belongs To When I go to the last author's posts And I follow "View" Then I should be on the last author's last post page - And the "Posts" tab should be selected + And the "Users" menu item should be selected Scenario: When the belongs to is optional Given a configuration of: """ ActiveAdmin.register User ActiveAdmin.register Post do - belongs_to :user, :optional => true + belongs_to :user, optional: true end """ When I go to the last author's posts - Then the "Users" tab should be selected + Then the "Users" menu item should be selected And I should see a menu item for "Posts" When I follow "Posts" - Then the "Posts" tab should be selected + Then the "Posts" menu item should be selected Scenario: Displaying belongs to resources in main menu Given a configuration of: @@ -63,4 +159,4 @@ Feature: Belongs To """ When I go to the last author's posts And I follow "View" - Then the "Posts" tab should be selected + Then the "Posts" menu item should be selected diff --git a/features/breadcrumb.feature b/features/breadcrumb.feature index b2d296cfee7..5b7334b7f36 100644 --- a/features/breadcrumb.feature +++ b/features/breadcrumb.feature @@ -1,3 +1,4 @@ +@breadcrumb Feature: Breadcrumb Background: @@ -35,7 +36,7 @@ Feature: Breadcrumb """ When I am on the new post page Then I should see "Post" - And I should not see the element ".breadcrumb" + And I should not see the element "nav[aria-label=breadcrumb]" Scenario: Application config of false and a resource config of true Given a configuration of: @@ -72,4 +73,4 @@ Feature: Breadcrumb end """ When I am on the new post page - Then I should not see the element ".breadcrumb" + Then I should not see the element "nav[aria-label=breadcrumb]" diff --git a/features/comments/commenting.feature b/features/comments/commenting.feature index de011d8bca2..5ba95570849 100644 --- a/features/comments/commenting.feature +++ b/features/comments/commenting.feature @@ -40,10 +40,10 @@ Feature: Commenting Given a configuration of: """ ActiveAdmin.application.namespace(:new_namespace).comments = false - ActiveAdmin.register Post, :namespace => :new_namespace - ActiveAdmin.register AdminUser, :namespace => :new_namespace + ActiveAdmin.register Post, namespace: :new_namespace + ActiveAdmin.register AdminUser, namespace: :new_namespace """ - Given I am logged in + And I am logged in When I am on the index page for posts in the new_namespace namespace And I follow "View" Then I should not see "Comments" @@ -52,12 +52,12 @@ Feature: Commenting Given a configuration of: """ ActiveAdmin.application.namespace(:new_namespace).comments = false - ActiveAdmin.register Post, :namespace => :new_namespace do + ActiveAdmin.register Post, namespace: :new_namespace do config.comments = true end - ActiveAdmin.register AdminUser, :namespace => :new_namespace + ActiveAdmin.register AdminUser, namespace: :new_namespace """ - Given I am logged in + And I am logged in When I am on the index page for posts in the new_namespace namespace And I follow "View" Then I should see "Comments" @@ -66,8 +66,8 @@ Feature: Commenting Given a show configuration of: """ ActiveAdmin.register Post - ActiveAdmin.register Post, :namespace => :public - ActiveAdmin.register AdminUser, :namespace => :public + ActiveAdmin.register Post, namespace: :public + ActiveAdmin.register AdminUser, namespace: :public """ When I add a comment "Hello world in admin namespace" Then I should see "Hello world in admin namespace" @@ -87,12 +87,12 @@ Feature: Commenting Scenario: Creating a comment on an aliased resource Given a configuration of: """ - ActiveAdmin.register Post, :as => "Article" + ActiveAdmin.register Post, as: "Article" """ - Given I am logged in + And I am logged in When I am on the index page for articles And I follow "View" - When I add a comment "Hello from Comment" + And I add a comment "Hello from Comment" Then I should see a flash with "Comment was successfully created" And I should be in the resource section for articles @@ -111,7 +111,7 @@ Feature: Commenting ActiveAdmin.register Post """ When I add a comment "Hello from Comment" - When I am on the index page for comments + And I am on the index page for comments Then I should see a table header with "Body" And I should see "Hello from Comment" @@ -120,11 +120,11 @@ Feature: Commenting """ ActiveAdmin.register User """ - Given I am logged in + And I am logged in And a publisher named "Pragmatic Publishers" exists When I am on the index page for users And I follow "View" - When I add a comment "Hello World" + And I add a comment "Hello World" Then I should see a flash with "Comment was successfully created" And I should be in the resource section for users When I am on the index page for comments @@ -136,36 +136,134 @@ Feature: Commenting """ ActiveAdmin.register Publisher """ - Given I am logged in + And I am logged in And a publisher named "Pragmatic Publishers" exists When I am on the index page for publishers And I follow "View" - When I add a comment "Hello World" + And I add a comment "Hello World" Then I should see a flash with "Comment was successfully created" And I should be in the resource section for publishers And I should see "Hello World" - Scenario: Commenting on a class with string id - Given a tag with the name "coolness" exists - Given a configuration of: - """ - ActiveAdmin.register Tag - """ - Given I am logged in - When I am on the index page for tags - And I follow "View" - When I add a comment "Tag Comment" - Then I should see a flash with "Comment was successfully created" - And I should be in the resource section for tags - Scenario: Commenting on an aliased resource with an existing non-aliased config Given a configuration of: """ ActiveAdmin.register Post ActiveAdmin.register Post, as: 'Foo' """ - Given I am logged in + And I am logged in When I am on the index page for foos And I follow "View" - When I add a comment "Bar" + And I add a comment "Bar" Then I should be in the resource section for foos + + Scenario: View comments + Given 70 comments added by admin with an email "admin@example.com" + And a show configuration of: + """ + ActiveAdmin.register Post + """ + Then I should see "Comments (70)" + And I should see "Displaying comments 1 - 25 of 70 in total" + And I should see 25 comments + And I should see pagination page 2 link + And I should see pagination page 3 link + And I should see the pagination "Next" link + When I follow "2" + Then I should see "Displaying comments 26 - 50 of 70 in total" + And I should see 25 comments + And I should see the pagination "Next" link + When I follow "Next" + Then I should see 20 comments + And I should see "Displaying comments 51 - 70 of 70 in total" + And I should not see the pagination "Next" link + + Scenario: Comments through explicit helper from custom controller + Given a post with the title "Hello World" written by "Jane Doe" exists + And a show configuration of: + """ + ActiveAdmin.register Post do + controller do + def show + @post = Post.find(params[:id]) + show! + end + end + + show do |post| + active_admin_comments_for(post) + end + end + """ + Then I should be able to add a comment + + @authorization + Scenario: Not authorized to list comments + Given 5 comments added by admin with an email "commenter@example.com" + And 3 comments added by admin with an email "admin@example.com" + And a show configuration of: + """ + class NoCommentListForASpecificUser < ActiveAdmin::AuthorizationAdapter + def authorized?(action, subject = nil) + if action == :read && subject == ActiveAdmin::Comment + user.email != "admin@example.com" + else + true + end + end + end + + ActiveAdmin.application.namespace(:admin).authorization_adapter = NoCommentListForASpecificUser + + ActiveAdmin.register Post + """ + Then I should not see "Comments" + And I should see 0 comments + And I should not be able to add a comment + + @authorization + Scenario: Authorized to list and view own comments + Given 5 comments added by admin with an email "commenter@example.com" + And 3 comments added by admin with an email "admin@example.com" + And a show configuration of: + """ + class ListCommentsByCurrentUserOnly < ActiveAdmin::AuthorizationAdapter + def scope_collection(collection, action = ActiveAdmin::Authorization::READ) + if collection.is_a?(ActiveRecord::Relation) && collection.klass == ActiveAdmin::Comment + collection.where(author: user) + else + collection + end + end + end + + ActiveAdmin.application.namespace(:admin).authorization_adapter = ListCommentsByCurrentUserOnly + + ActiveAdmin.register Post + """ + Then I should see "Comments (3)" + And I should see 3 comments + And I should be able to add a comment + + @authorization + Scenario: Not authorized to create comments + Given 5 comments added by admin with an email "commenter@example.com" + And a show configuration of: + """ + class NoNewComments < ActiveAdmin::AuthorizationAdapter + def authorized?(action, subject = nil) + if (action == :new || action == :create) && subject == ActiveAdmin::Comment + false + else + true + end + end + end + + ActiveAdmin.application.namespace(:admin).authorization_adapter = NoNewComments + + ActiveAdmin.register Post + """ + Then I should see "Comments (5)" + And I should see 5 comments + And I should not be able to add a comment diff --git a/features/comments/viewing_index.feature b/features/comments/viewing_index.feature index a18263b31d9..f30387c6cd0 100644 --- a/features/comments/viewing_index.feature +++ b/features/comments/viewing_index.feature @@ -1,19 +1,18 @@ Feature: Viewing Index of Comments - Background: + Scenario: Viewing all comments for a namespace Given a post with the title "Hello World" written by "Jane Doe" exists - Given a show configuration of: + And a show configuration of: """ ActiveAdmin.register Post """ - Scenario: Viewing all commments for a namespace When I add a comment "Hello from Comment" - When I am on the index page for comments + And I am on the index page for comments Then I should see a table header with "Body" And I should see a table header with "Resource" And I should see a table header with "Author" And I should see "Hello from Comment" And I should see a link to "Hello World" And I should see "admin@example.com" - And I should not see an action item button "New Comment" + And I should not see an action item link to "New Comment" diff --git a/features/create_another.feature b/features/create_another.feature new file mode 100644 index 00000000000..c4b68feda50 --- /dev/null +++ b/features/create_another.feature @@ -0,0 +1,55 @@ +Feature: Create Another checkbox + + Background: + Given I am logged in + + Scenario: On a new page + Given a configuration of: + """ + ActiveAdmin.register Post do + config.create_another = true + + permit_params :custom_category_id, :author_id, :title, + :body, :position, :published_date, :starred + end + """ + Then I am on the index page for posts + And I follow "New Post" + When I fill in "Title" with "Hello World" + And I fill in "Body" with "This is the body" + And the "Create another Post" checkbox should not be checked + And I check "Create another" + And I press "Create Post" + Then I should see "Post was successfully created." + And I should see "New Post" + And the "Create another" checkbox should be checked + When I fill in "Title" with "Another Hello World" + And I fill in "Body" with "This is the another body" + And I uncheck "Create another" + And I press "Create Post" + Then I should see "Post was successfully created." + And I should see the attribute "Title" with "Another Hello World" + And I should see the attribute "Body" with "This is the another body" + + Scenario: Application config of false and a resource config of true + Given a configuration of: + """ + ActiveAdmin.application.create_another = false + ActiveAdmin.register Post do + config.create_another = true + end + """ + When I am on the new post page + Then I should see the element ".input-create-another" + And the "Create another Post" checkbox should not be checked + + Scenario: Application config of true and a resource config of false + Given a configuration of: + """ + ActiveAdmin.application.create_another = true + ActiveAdmin.register Post do + config.create_another = false + end + """ + When I am on the new post page + Then I should not see the element ".input-create-another" diff --git a/features/dashboard.feature b/features/dashboard.feature deleted file mode 100644 index b62867f3647..00000000000 --- a/features/dashboard.feature +++ /dev/null @@ -1,16 +0,0 @@ -Feature: Dashboard - - Scenario: With default configuration - Given a configuration of: - """ - ActiveAdmin.register_page "Dashboard" do - content do - para "Hello world from the dashboard page" - end - end - """ - Given I am logged in - When I go to the dashboard - Then I should see the Active Admin layout - And I should not see the default welcome message - And I should see "Hello world from the dashboard page" diff --git a/features/decorators.feature b/features/decorators.feature index 8d00c98ea9a..d7f11c63081 100644 --- a/features/decorators.feature +++ b/features/decorators.feature @@ -4,7 +4,7 @@ Feature: Decorators Background: Given a user named "John Doe" exists - Given a post with the title "A very unique post" exists + And a post with the title "A very unique post" exists And I am logged in Scenario: Index page with decorator @@ -17,12 +17,33 @@ Feature: Decorators column(:id) column(:title) column(:decorator_method) + column(:starred) end end """ When I am on the index page for posts Then I should see "A method only available on the decorator" And I should see "A very unique post" + And I should see "No" + + Scenario: Index page with PORO decorator + Given a configuration of: + """ + ActiveAdmin.register Post do + decorate_with PostPoroDecorator + + index do + column(:id) + column(:title) + column(:decorator_method) + column(:starred) + end + end + """ + When I am on the index page for posts + Then I should see "A method only available on the PORO decorator" + And I should see "A very unique post" + And I should see "No" Scenario: Show page with decorator Given a configuration of: @@ -31,7 +52,7 @@ Feature: Decorators decorate_with PostDecorator show do - attributes_table :title, :decorator_method + attributes_table_for(resource, :title, :decorator_method) end end """ diff --git a/features/development_reloading.feature b/features/development_reloading.feature index 56a2eae2fb0..d31c990bc64 100644 --- a/features/development_reloading.feature +++ b/features/development_reloading.feature @@ -1,10 +1,10 @@ +@requires-reloading Feature: Development Reloading In order to quickly develop applications As a developer I want the application to reload itself in development - @requires-reloading Scenario: Registering a resource that was not previously registered When I am logged in with capybara Then I should not see a menu item for "Posts" @@ -12,19 +12,17 @@ Feature: Development Reloading When "app/admin/posts.rb" contains: """ ActiveAdmin.register Post do - if Rails::VERSION::MAJOR == 4 - permit_params :custom_category_id, :author_id, :title, - :body, :position, :published_at, :starred - end + permit_params :custom_category_id, :author_id, :title, + :body, :position, :published_date, :starred end """ - When I am logged in with capybara + And I am logged in with capybara Then I should see a menu item for "Posts" When I create a new post with the title "A" - Then I should see a successful create flash + Then I should see a flash with "Post was successfully created" When I add "validates_presence_of :title" to the "post" model And I create a new post with the title "" - Then I should not see a successful create flash + Then I should not see "Post was successfully created" And I should see a validation error "can't be blank" diff --git a/features/edit_page.feature b/features/edit_page.feature index 704c8beb844..03f36a1b717 100644 --- a/features/edit_page.feature +++ b/features/edit_page.feature @@ -6,25 +6,25 @@ Feature: Edit Page 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 a tag named "Bugs" exists And I am logged in + + Scenario: Default form with no config Given a configuration of: """ ActiveAdmin.register Post do - if Rails::VERSION::MAJOR == 4 - permit_params :custom_category_id, :author_id, :title, - :body, :position, :published_at, :starred - end + permit_params :custom_category_id, :author_id, :title, + :body, :position, :published_date, :starred end """ When I am on the index page for posts - - Scenario: Default form with no config - Given I follow "Edit" + And I follow "Edit" Then the "Title" field should contain "Hello World" And the "Body" field should contain "" And the "Category" field should contain "" And the "Author" field should contain the option "John Doe" When I fill in "Title" with "Hello World from update" + Then I should not see the element "Create another" When I press "Update Post" Then I should see "Post was successfully updated." And I should see the attribute "Title" with "Hello World from update" @@ -34,7 +34,7 @@ Feature: Edit Page Given a configuration of: """ ActiveAdmin.register Post do - permit_params :category, :author, :title, :body, :published_at, :starred if Rails::VERSION::MAJOR == 4 + permit_params :category, :author, :title, :body, :published_date, :starred form do |f| f.inputs "Your Post" do @@ -42,76 +42,112 @@ Feature: Edit 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 "Edit" + When I am on the index page for posts + And I follow "Edit" Then I should see a fieldset titled "Your Post" And I should see a fieldset titled "Publishing" And the "Title" field should contain "Hello World" And the "Body" field should contain "" When I fill in "Title" with "Hello World from update" - When I press "Update Post" + 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" - Scenario: Generating a custom form with :html set, visiting the new page first (bug probing issue #109) + Scenario: Generating a custom form with :html set, visiting the new page first Given a configuration of: """ ActiveAdmin.register Post do - permit_params :category, :author, :title, :body, :published_at, :starred if Rails::VERSION::MAJOR == 4 + permit_params :category, :author, :title, :body, :published_date, :starred - form :html => {} do |f| + form html: {} 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" + When I am on the index page for posts + And I follow "New" Then I follow "Posts" - Then I follow "Edit" - Then I should see a fieldset titled "Your Post" + And I follow "Edit" + And I should see a fieldset titled "Your Post" And I should see a fieldset titled "Publishing" And the "Title" field should contain "Hello World" And the "Body" field should contain "" When I fill in "Title" with "Hello World from update" - When I press "Update Post" + 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" + @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 - permit_params :category, :author, :title, :body, :published_at, :starred if Rails::VERSION::MAJOR == 4 + permit_params :category, :author, :title, :body, :published_date, :starred - form :partial => "form" + form partial: "form" end """ - Given I follow "Edit" + When I am on the index page for posts + And I follow "Edit" Then the "Title" field should contain "Hello World" And the "Body" field should contain "" When I fill in "Title" with "Hello World from update" - When I press "Update Post" + 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" + + Scenario: Generating a custom form for Tag resource + Given a configuration of: + """ + ActiveAdmin.register Tag do + form do |f| + f.inputs "Details" do + f.input :name + end + f.actions + end + end + """ + When I am on the index page for tags + And I follow "Edit" + Then I should see a fieldset titled "Details" + And the "Name" field should contain "Bugs" + + Scenario: Save resource within a transaction + Given a company named "My company" with a store named "First store" exists + And a store named "Second store" exists + When I am on the index page for companies + And I follow "Edit" + Then the "Stores" select should have "First store" selected + When I fill in "Name" with "" + And I select "Second store" from "Stores" + And I press "Update Company" + Then I should see "can't be blank" + When I press "Cancel" + And I follow "View" + Then I should see the attribute "Stores" with "First store" + And I should not see the attribute "Stores" with "Second store" diff --git a/features/favicon.feature b/features/favicon.feature deleted file mode 100644 index be240533016..00000000000 --- a/features/favicon.feature +++ /dev/null @@ -1,20 +0,0 @@ -Feature: Favicon - - Configuring a Favicon file - - Background: - Given a configuration of: - """ - ActiveAdmin.register Post - ActiveAdmin.application.favicon = "a/favicon.ico" - """ - - Scenario: Logged out views show Favicon - Given I am logged out - When I am on the login page - Then I should see the favicon "a/favicon.ico" - - Scenario: Logged in views show Favicon - Given I am logged in - When I am on the dashboard - Then I should see the favicon "a/favicon.ico" \ No newline at end of file diff --git a/features/filter_attributes.feature b/features/filter_attributes.feature new file mode 100644 index 00000000000..0f1c499943f --- /dev/null +++ b/features/filter_attributes.feature @@ -0,0 +1,46 @@ +Feature: Filter Attributes + + Filtering sensitive attributes + + Background: + Given a configuration of: + """ + ActiveAdmin.register User + """ + And I am logged in + And a user named "John Doe" exists + And I am on the index page for users + + Scenario: Default index page + Then I should not see "Encrypted" + But I should see "Age" + + Scenario: Default new form + Given I follow "New User" + Then I should not see "Encrypted" + But I should see "Age" + + Scenario: Default edit form + Given I follow "Edit" + Then I should not see "Encrypted" + But I should see "Age" + + Scenario: Default show page + Given I follow "View" + Then I should not see "Encrypted" + But I should see "Age" + + Scenario: Default CSV export + Given I follow "CSV" + Then I should not see "Encrypted" + But I should see "Age" + + # TODO: JSON + # Scenario: Default JSON + # Given I follow "JSON" + # Then I should not see "encrypted" + # But I should see "age" + + Scenario: Default XML + Given I follow "XML" + Then I should not see "encrypted" diff --git a/features/first_boot.feature b/features/first_boot.feature index 51aeab1ab35..700a3ddc4fd 100644 --- a/features/first_boot.feature +++ b/features/first_boot.feature @@ -10,7 +10,7 @@ Feature: First Boot """ And an admin user "admin@example.com" exists When I go to the dashboard - When I fill in "Email" with "admin@example.com" + And 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 diff --git a/features/global_navigation.feature b/features/global_navigation.feature index aa93d6559a7..a05b1e52c8a 100644 --- a/features/global_navigation.feature +++ b/features/global_navigation.feature @@ -5,25 +5,25 @@ Feature: Global Navigation """ ActiveAdmin.register Post """ - Given I am logged in + And I am logged in And 10 posts exist Scenario: Viewing the current section in the global navigation Given I am on the index page for posts - Then the "Posts" tab should be selected + Then the "Posts" menu item should be selected Scenario: Viewing the current section in the global navigation when on new page Given I am on the index page for posts And I follow "New Post" - Then the "Posts" tab should be selected + Then the "Posts" menu item should be selected Scenario: Viewing the current section in the global navigation when on show page Given I am on the index page for posts And I follow "View" - Then the "Posts" tab should be selected + Then the "Posts" menu item should be selected Scenario: Viewing the current section in the global navigation when on edit page Given I am on the index page for posts And I follow "View" And I follow "Edit Post" - Then the "Posts" tab should be selected + Then the "Posts" menu item should be selected diff --git a/features/has_many.feature b/features/has_many.feature new file mode 100644 index 00000000000..9f82cf2d608 --- /dev/null +++ b/features/has_many.feature @@ -0,0 +1,79 @@ +@javascript +Feature: Has Many + + A resource has many other resources + + Background: + Given I am logged in + And a post with the title "Hello World" written by "John Doe" exists + + Scenario: Updating the parent resource page with default text for Remove button + Given a configuration of: + """ + ActiveAdmin.register User do + form do |f| + f.inputs do + f.has_many :posts do |ff| + ff.input :title + ff.input :body + end + end + f.actions + end + end + ActiveAdmin.register Post + """ + When I go to the last author's show page + And I follow "Edit User" + Then I should see a link to "Add New Post" + + When I click "Add New Post" + Then I should see a link to "Remove" + + Scenario: Updating the parent resource page with custom text for Remove button + Given a configuration of: + """ + ActiveAdmin.register User do + form do |f| + f.inputs do + f.has_many :posts, remove_record: "Hide" do |ff| + ff.input :title + ff.input :body + end + end + f.actions + end + end + ActiveAdmin.register Post + """ + When I go to the last author's show page + And I follow "Edit User" + Then I should see a link to "Add New Post" + + When I click "Add New Post" + Then I should see a link to "Hide" + + # FIXME: This will depend on contribution from the community + # Scenario: Sortable is initialized when transitioning to edit with existing data + # Given a configuration of: + # """ + # ActiveAdmin.register User do + # form do |f| + # f.inputs do + # f.has_many :posts, sortable: :position do |ff| + # ff.input :title + # ff.input :body + # end + # end + # f.actions + # end + # end + # ActiveAdmin.register Post + # """ + # When I go to the last author's show page + # And I follow "Edit User" + # Then I should see a link to "Add New Post" + # And there should be 1 "input" tag within ".ui-sortable" + + # When I click "Add New Post" + # Then I should see a link to "Remove" diff --git a/features/i18n.feature b/features/i18n.feature index 3948be7b745..faadf0542ac 100644 --- a/features/i18n.feature +++ b/features/i18n.feature @@ -1,3 +1,4 @@ +@locale_manipulation Feature: Internationalization ActiveAdmin should use the translations provided by the host app. @@ -10,11 +11,10 @@ Feature: Internationalization When I follow "Bookstores" Then I should see the page title "Bookstores" - Then I should see "Hello words" + And I should see "Hello words" When I follow "View" - Then I should see "Bookstore Details" - And I should see "Hello words" + Then I should see "Hello words" And I should see a link to "Delete Bookstore" When I follow "Edit Bookstore" @@ -28,16 +28,16 @@ Feature: Internationalization When I set my locale to "fr" And I go to the dashboard Then I should see "Store" - Then I should see "Déconnexion" + And I should see "Déconnexion" When I set my locale to "en" And I go to the dashboard Then I should see "Bookstore" - Then I should see "Logout" + And I should see "Sign out" Scenario: Overriding translations Given I am logged in And a store named "Hello words" exists When I go to the dashboard - When I follow "Bookstores" - Then I should see "Download this:" + And I follow "Bookstores" + Then I should see "Export:" diff --git a/features/index/batch_actions.feature b/features/index/batch_actions.feature index 92e7031a6e8..e1fa17d3f06 100644 --- a/features/index/batch_actions.feature +++ b/features/index/batch_actions.feature @@ -9,18 +9,42 @@ Feature: Batch Actions """ Then I should see the batch action button And I should see that the batch action button is disabled - And I should see the batch action popover exists + And I should see the batch action popover And I should see 10 posts in the table When I check the 1st record And I check the 2nd record - And I follow "Batch Actions" + And I press "Batch Actions" Then I should see the batch action :destroy "Delete Selected" - Given I click "Delete Selected" and accept confirmation - Then I should see a flash with "Successfully destroyed 2 posts" + When I click "Delete Selected" and accept confirmation + Then I should see a flash with "Successfully deleted 2 posts" And I should see 8 posts in the table + @javascript + Scenario: Use default (destroy) batch action when default_url_options present + Given 3 posts exist + And an index configuration of: + """ + ActiveAdmin.register Post do + controller do + protected + + def default_url_options + { locale: I18n.locale } + end + end + end + """ + When I check the 1st record + And I press "Batch Actions" + Then I should see the batch action :destroy "Delete Selected" + + When I click "Delete Selected" and accept confirmation + Then I should see a flash with "Successfully deleted 1 post" + And I should see 2 posts in the table + + @javascript Scenario: Use default (destroy) batch action on a decorated resource Given 5 posts exist And an index configuration of: @@ -31,11 +55,29 @@ Feature: Batch Actions """ When I check the 2nd record And I check the 4th record - And I follow "Batch Actions" + And I press "Batch Actions" + Then I should see the batch action :destroy "Delete Selected" + + When I click "Delete Selected" and accept confirmation + Then I should see a flash with "Successfully deleted 2 posts" + And I should see 3 posts in the table + + @javascript + Scenario: Use default (destroy) batch action on a PORO decorated resource + Given 5 posts exist + And an index configuration of: + """ + ActiveAdmin.register Post do + decorate_with PostPoroDecorator + end + """ + When I check the 2nd record + And I check the 4th record + And I press "Batch Actions" Then I should see the batch action :destroy "Delete Selected" - Given I submit the batch action form with "destroy" - Then I should see a flash with "Successfully destroyed 2 posts" + When I click "Delete Selected" and accept confirmation + Then I should see a flash with "Successfully deleted 2 posts" And I should see 3 posts in the table @javascript @@ -52,25 +94,38 @@ Feature: Batch Actions When I go to the last author's posts Then I should see the batch action button And I should see that the batch action button is disabled - And I should see the batch action popover exists + And I should see the batch action popover And I should see 5 posts in the table When I check the 2nd record And I check the 4th record - And I follow "Batch Actions" + And I press "Batch Actions" Then I should see the batch action :destroy "Delete Selected" - Given I click "Delete Selected" and accept confirmation - Then I should see a flash with "Successfully destroyed 2 posts" + When I click "Delete Selected" and accept confirmation + Then I should see a flash with "Successfully deleted 2 posts" And I should see 3 posts in the table + Scenario: Disable display of batch action button if all nested buttons hide + Given 1 post exist + And an index configuration of: + """ + ActiveAdmin.register Post do + batch_action :destroy, false + batch_action(:flag, if: proc { false } ) do + render text: 42 + end + end + """ + Then I should not see the batch action selector + Scenario: Using a custom batch action Given 10 posts exist And an index configuration of: """ ActiveAdmin.register Post do batch_action(:flag) do - redirect_to collection_path, :notice => "Successfully flagged 10 posts" + redirect_to collection_path, notice: "Successfully flagged 10 posts" end end """ @@ -106,8 +161,8 @@ Feature: Batch Actions And an index configuration of: """ ActiveAdmin.register Post do - batch_action(:flag, :if => proc { true }) {} - batch_action(:unflag, :if => proc { false }) {} + batch_action(:flag, if: proc { true }) {} + batch_action(:unflag, if: proc { false }) {} end """ Then I should see the batch action :flag "Flag Selected" @@ -118,9 +173,9 @@ Feature: Batch Actions And an index configuration of: """ ActiveAdmin.register Post do - batch_action(:test, :priority => 3) {} - batch_action(:flag, :priority => 2) {} - batch_action(:unflag, :priority => 1) {} + batch_action(:test, priority: 3) {} + batch_action(:flag, priority: 2) {} + batch_action(:unflag, priority: 1) {} end """ Then the 4th batch action should be "Delete Selected" @@ -140,44 +195,47 @@ Feature: Batch Actions Then I should see the batch action :very_complex_and_time_consuming "Very Complex and Time Consuming Selected" And I should see the batch action :passing_a_symbol "Passing A Symbol Selected" - Scenario: Use a Form with text - Given 10 posts exist - And an index configuration of: - """ - ActiveAdmin.register Post do - batch_action :destroy, false - batch_action(:action_with_form, form: { name: :text }) {} - end - """ - - When I check the 1st record - And I follow "Batch Actions" - Then I should be show a input with name "name" and type "text" - - Scenario: Use a Form with select + @javascript + Scenario: Using a custom batch action with form as Hash Given 10 posts exist And an index configuration of: """ ActiveAdmin.register Post do - batch_action :destroy, false - batch_action(:action_with_form, form: { type: ["a", "b"] }) {} + 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| + if inputs["starred"].present? + redirect_to collection_path, notice: "Successfully flagged 10 posts" + else + redirect_to collection_path, notice: "Didn't flag any posts" + end + end end """ + When I check the 2nd record + And I press "Batch Actions" + And I click "Set Starred" + Then I should see "Toggle Starred" + And I should see the field "Starred" of type "checkbox" + And I check "Starred" - When I check the 1st record - And I follow "Batch Actions" - Then I should be show a select with name "type" with the values "a, b" + When I press "Submit" + Then I should see a flash with "Successfully flagged 10 posts" - Scenario: Use a Form with select values from proc + @javascript + Scenario: Use batch action without confirmation Given 10 posts exist And an index configuration of: """ ActiveAdmin.register Post do - batch_action :destroy, false - batch_action(:action_with_form, form: ->{ {type: ["a", "b"]} }) {} + batch_action :mark_not_starred do |ids| + Post.where(id: ids).update_all(starred: false) + redirect_to collection_path, notice: "#{ids.count} posts marked as not starred" + end end """ - When I check the 1st record - And I follow "Batch Actions" - Then I should be show a select with name "type" with the values "a, b" + And I check the 3rd record + And I press "Batch Actions" + Then I should see the batch action :mark_not_starred "Mark Not Starred" + When I click "Mark Not Starred" + Then I should see a flash with "2 posts marked as not starred" + And I should see 10 posts in the table diff --git a/features/index/filters.feature b/features/index/filters.feature index 3c80f0bebf2..64768e09519 100644 --- a/features/index/filters.feature +++ b/features/index/filters.feature @@ -1,3 +1,4 @@ +@filters Feature: Index Filtering Scenario: Default Resources Filters @@ -7,20 +8,32 @@ 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" And I should see the following filters: - | Author | select | - | Category | select | - | Title | string | - | Body | string | - | Published at | date range | - | Created at | date range | - | Updated at | date range | + | Author | select | + | Category | select | + | Title | string | + | Body | string | + | Published date | date range | + | Created at | date range | + | Updated at | date range | 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" + And I should see "Hello World 2" in the table + And I should see current filter "title_cont" equal to "Hello World 2" with label "Title contains" + + Scenario: No XSS in Resources Filters + Given an index configuration of: + """ + ActiveAdmin.register Post do + filter :title + end + """ + When I fill in "Title" with "" + 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,7 +122,7 @@ 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 @@ -102,20 +134,20 @@ Feature: Index Filtering 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" within ".index_table" - And I should not see "Mystery" within ".index_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 filter preserves custom parameters + 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 - ActiveAdmin.application.favicon = false """ - Then I should see "Displaying all 2 Categories" + 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" @@ -134,14 +166,15 @@ Feature: Index Filtering And an index configuration of: """ ActiveAdmin.register Category do - filter :authors, :as => :check_boxes + filter :authors, as: :check_boxes end """ When I press "Filter" Then I should see 2 posts in the table - And I should see "Mystery" within ".index_table" - And I should see "Non-Fiction" within ".index_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 @@ -149,13 +182,196 @@ Feature: Index Filtering And an index configuration of: """ ActiveAdmin.register Category do - filter :authors, :as => :check_boxes + 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" within ".index_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 + 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 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" + + 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 cf18a42bba9..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 | Position | 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 | Position | 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, :byte_order_mark => "" 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 @@ -116,12 +192,12 @@ Feature: Format as CSV | 012345 | (.*) | And the CSV file should contain "012345" in quotes - Scenario: Without CVS column names explicitely specified + Scenario: Without CSV column names explicitly specified 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 :column_names => true do + csv column_names: true do column :title column :body end @@ -134,12 +210,12 @@ Feature: Format as CSV | Title | Body | | 012345 | (.*) | - Scenario: Without CVS column names + Scenario: Without CSV column names 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 :column_names => false do + csv column_names: false do column :title column :body end @@ -148,14 +224,20 @@ Feature: Format as CSV 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: + 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 + csv encoding: 'SJIS' do column :title column :body end @@ -164,12 +246,12 @@ Feature: Format as CSV And a post with the title "あいうえお" exists When I am on the index page for posts And I follow "CSV" - And the encoding of the CSV file should be "SJIS" + 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.application.csv_options = { encoding: 'SJIS' } ActiveAdmin.register Post do csv do column :title @@ -180,7 +262,7 @@ Feature: Format as CSV And a post with the title "あいうえお" exists When I am on the index page for posts And I follow "CSV" - And the encoding of the CSV file should be "SJIS" + Then the encoding of the CSV file should be "SJIS" Scenario: With decorator Given a configuration of: @@ -198,6 +280,6 @@ 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: | 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 ad8a0474adf..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 @@ -42,7 +42,7 @@ Feature: Index Formats Given an index configuration of: """ ActiveAdmin.register Post do - index :download_links => proc { false } + index download_links: proc { false } end """ And 1 post exists @@ -51,11 +51,20 @@ Feature: Index Formats 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] } + index download_links: -> { [:csv] } end """ And 1 post exists @@ -64,3 +73,16 @@ 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 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 68b171f2ca5..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 a blog header "Hello World" - 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 a blog header "Hello World" - 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 a blog header "Hello World From Block" - 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 8aeab738590..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: @@ -116,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 @@ -135,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 @@ -146,46 +167,6 @@ 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: Actions with defaults and custom actions within a dropdown - 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, :show, :edit, :update - - index do - column :category - actions dropdown: true do |resource| - item 'Custom Action', edit_admin_post_path(resource) - end - end - end - """ - Then I should see a dropdown menu item to "View" - And I should see a dropdown menu item to "Edit" - And I should not see a dropdown menu item to "Delete" - And I should see a dropdown menu item to "Custom Action" - - Scenario: Actions without default actions within a dropdown - 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, :show, :edit, :update - - index do - column :category - actions :defaults => false, dropdown: true do |resource| - item 'Custom Action', edit_admin_post_path(resource) - end - end - end - """ - Then I should not see a dropdown menu item to "View" - And I should not see a dropdown menu item to "Edit" - And I should not see a dropdown menu item to "Delete" - And I should see a dropdown menu item 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: @@ -218,14 +199,14 @@ Feature: Index as Table ActiveAdmin.register Post do index do column :author_id do end - column 'published_at' do end + column 'published_date' do end column :category do end end end """ Then I should see a sortable table header with "Author" - Then I should see a sortable table header with "published_at" - Then I should not see a sortable table header with "Category" + 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 @@ -234,12 +215,12 @@ Feature: Index as Table ActiveAdmin.register Post do index do column :author_id, sortable: false do end - column 'published_at', sortable: false do end + column 'published_date', sortable: false do end end end """ Then I should not see a sortable table header with "Author" - Then I should not see a sortable table header with "published_at" + 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 @@ -250,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 | Position | Starred | Created At | Updated At | | - | [ ] | 2 | Bye bye world | Move your... | | | No | /.*/ | /.*/ | ViewEditDelete | - | [ ] | 1 | Hello World | From the body | | | No | /.*/ | /.*/ | 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 | Position | Starred | Created At | Updated At | | - | [ ] | 1 | Hello World | From the body | | | No | /.*/ | /.*/ | ViewEditDelete | - | [ ] | 2 | Bye bye world | Move your... | | | No | /.*/ | /.*/ | 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 @@ -274,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 2a8acfe901f..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,37 +30,6 @@ 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 - Given an index configuration of: - """ - ActiveAdmin.register Post do - index :as => :grid do |post| - h2 auto_link(post) - end - end - """ - And I should see "There are no Posts yet. Create one" - - Scenario: Viewing a index using blocks with no resources - Given an index configuration of: - """ - ActiveAdmin.register Post do - index :as => :block do |post| - span(link_to(post.title, admin_post_path(post))) - 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" - Scenario: Customizing the default table with no resources Given an index configuration of: """ diff --git a/features/index/index_parameters.feature b/features/index/index_parameters.feature index 92021771040..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 @@ -35,41 +35,41 @@ Feature: Index Parameters Given a configuration of: """ ActiveAdmin.application.namespace(:superadmin).download_links = false - ActiveAdmin.register AdminUser, :namespace => :superadmin + 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.application.namespace(:superadmin).download_links = false - ActiveAdmin.register AdminUser, :namespace => :superadmin + 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 cdb7c87de55..3a9c8e1a854 100644 --- a/features/index/index_scope_to.feature +++ b/features/index/index_scope_to.feature @@ -6,6 +6,8 @@ Feature: Index Scope To 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 @@ -17,12 +19,32 @@ Feature: Index Scope To # 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 """ + 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 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 @@ -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/page_title.feature b/features/index/page_title.feature index 277f24a871c..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,7 @@ 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" @@ -34,7 +34,7 @@ Feature: Index - Page Title """ ActiveAdmin.register Post do controller do - before_filter { @page_title = "List of #{resource_class.model_name.plural}" } + before_action { @page_title = "List of #{resource_class.model_name.plural}" } end end """ diff --git a/features/index/pagination.feature b/features/index/pagination.feature index e3484c5b65b..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,7 +40,7 @@ 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 @@ -48,16 +49,16 @@ Feature: Index Pagination """ ActiveAdmin.register Post do config.per_page = 10 - index :pagination_total => false do + index pagination_total: false do end end """ - Given 11 posts exist + And 11 posts exist When I am on the index page for posts - Then I should see "Displaying Posts 1 - 10" - And I should not see "11 in total" + 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 "Displaying Posts 11 - 11" + 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 f06494a14fb..4d7c381b42c 100644 --- a/features/index/switch_index_view.feature +++ b/features/index/switch_index_view.feature @@ -5,69 +5,51 @@ Feature: Switch Index View 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: """ 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 + index as: CustomIndexView 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" + 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 + index default: true do column :title end end """ - Then I should see "Hello World from Table" within ".index_as_table" + 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" - - 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 => :block do |post| - span(link_to(post.title, admin_post_path(post))) - end - index :as => :table, :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" - - - + 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/meta_tags.feature b/features/meta_tags.feature deleted file mode 100644 index f94e24d6682..00000000000 --- a/features/meta_tags.feature +++ /dev/null @@ -1,21 +0,0 @@ -Feature: Meta Tag - - Add custom meta tags to head of pages. - - Background: - Given a configuration of: - """ - ActiveAdmin.register Post - ActiveAdmin.application.meta_tags = { author: 'My Company' } - ActiveAdmin.application.meta_tags_for_logged_out_pages = { robots: 'noindex' } - """ - - Scenario: Logged out views include custom meta tags - Given I am logged out - When I am on the login page - Then the site should contain a meta tag with name "robots" and content "noindex" - - Scenario: Logged in views include custom meta tags - Given I am logged in - When I am on the dashboard - Then the site should contain a meta tag with name "author" and content "My Company" diff --git a/features/new_page.feature b/features/new_page.feature index 08162415b01..6548476755f 100644 --- a/features/new_page.feature +++ b/features/new_page.feature @@ -4,15 +4,13 @@ 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 do - if Rails::VERSION::MAJOR == 4 - permit_params :custom_category_id, :author_id, :title, - :body, :position, :published_at, :starred - end + permit_params :custom_category_id, :author_id, :title, + :body, :position, :published_date, :starred end """ When I am on the index page for posts @@ -27,14 +25,14 @@ Feature: New Page 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 "Category" with "Music" And I should see the attribute "Author" with "John Doe" Scenario: Generating a custom form Given a configuration of: """ ActiveAdmin.register Post do - permit_params :custom_category_id, :author_id, :title, :body, :published_at, :starred if Rails::VERSION::MAJOR == 4 + permit_params :custom_category_id, :author_id, :title, :body, :published_date, :starred form do |f| f.inputs "Your Post" do @@ -42,13 +40,13 @@ 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" + 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" @@ -58,25 +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 - permit_params :custom_category_id, :author_id, :title, :body, :published_at, :starred if Rails::VERSION::MAJOR == 4 + permit_params :custom_category_id, :author_id, :title, :body, :published_date, :starred - form :partial => "form" + 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." @@ -87,7 +145,7 @@ Feature: New Page Given a configuration of: """ ActiveAdmin.register Post do - permit_params :custom_category_id, :author_id, :title, :body, :published_at, :starred if Rails::VERSION::MAJOR == 4 + permit_params :custom_category_id, :author_id, :title, :body, :published_date, :starred form do |f| f.inputs "Your Post" do @@ -98,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 5391e2538d7..00000000000 --- a/features/registering_assets.feature +++ /dev/null @@ -1,34 +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 "active_admin.css" - 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 7d4b32eab96..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,8 +83,7 @@ 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: @@ -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 index 9ce8c065ac2..86d986ebfce 100644 --- a/features/renamed_resource.feature +++ b/features/renamed_resource.feature @@ -1,25 +1,21 @@ Feature: Renamed Resource - Strong attributes for resources renamed with as: 'NewName' + Resources renamed with as: 'NewName' - Background: + Scenario: Default form with no config 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 Blog::Post, as: 'Post' do - if Rails::VERSION::MAJOR == 4 - permit_params :custom_category_id, :author_id, :title, - :body, :position, :published_at, :starred - end + permit_params :custom_category_id, :author_id, :title, + :body, :position, :published_date, :starred end """ When I am on the index page for posts - - Scenario: Default form with no config - Given I follow "New Post" - When I fill in "Title" with "Hello World" + 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" @@ -27,6 +23,48 @@ Feature: Renamed Resource 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 "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/columns.feature b/features/show/columns.feature deleted file mode 100644 index 397cf7488ff..00000000000 --- a/features/show/columns.feature +++ /dev/null @@ -1,40 +0,0 @@ -Feature: Show - Columns - - Columns in show page - - Background: - Given a post with the title "Hello World" written by "Jane Doe" exists - - Scenario: - Given a show configuration of: - """ - ActiveAdmin.register Post do - - show do - columns do - end - end - - end - """ - Then I should see a columns container - And I should see 0 column - - Scenario: - Given a show configuration of: - """ - ActiveAdmin.register Post do - - show do - columns do - column do - end - column do - end - end - end - - end - """ - Then I should see a columns container - And I should see 2 columns diff --git a/features/show/default_content.feature b/features/show/default_content.feature index e2d8fbd7e4c..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 unstarred 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,9 +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 the attribute "Starred" with "false" - 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: @@ -33,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 @@ -42,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 bc9bf56f6d1..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" @@ -51,7 +51,7 @@ Feature: Show - Page Title """ ActiveAdmin.register Post do controller do - before_filter { @page_title = "List of #{resource_class.model_name.plural}" } + before_action { @page_title = "List of #{resource_class.model_name.plural}" } end end """ 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/show/tabs.feature b/features/show/tabs.feature deleted file mode 100644 index 3a8cfe6f6da..00000000000 --- a/features/show/tabs.feature +++ /dev/null @@ -1,27 +0,0 @@ -Feature: Show - Tabs - - Add tabs with different content to the page - - Background: - Given a post with the title "Hello World" written by "Jane Doe" exists - - Scenario: Set a method to be called on the resource as the title - Given a show configuration of: - """ - ActiveAdmin.register Post do - show do - tabs do - tab :overview do - span "tab 1" - end - - tab :details do - span "tab 2" - end - end - end - end - """ - Then I should see two tabs "Overview" and "Details" - And I should see "tab 1" - And I should see "tab 2" diff --git a/features/sidebar_sections.feature b/features/sidebar_sections.feature index ee4dafb52d8..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,21 +177,21 @@ 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: """ @@ -207,4 +202,4 @@ Feature: Sidebar Sections end """ When I am on the index page for posts - Then I should see a sidebar titled "Help" above sidebar titled "Filters" + 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 fd132d41819..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 = "/admin" """ 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 the site title "My Great Site" - 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 3f91d19d9fd..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(:import, :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(:review, :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(:review, :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 d76dd34a564..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| - expect(page).to 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| - expect(page).to_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 bcc030208eb..9531ac3f540 100644 --- a/features/step_definitions/action_link_steps.rb +++ b/features/step_definitions/action_link_steps.rb @@ -1,20 +1,12 @@ -Then /^I should see a member link to "([^"]*)"$/ do |name| - expect(page).to 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 +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 - -Then /^I should see a dropdown menu item to "([^"]*)"$/ do |name| - expect(page).to have_css('ul.dropdown_menu_list li a', text: name) -end - -Then /^I should not see a dropdown menu item to "([^"]*)"$/ do |name| - %{Then I should not see "#{name}" within "ul.dropdown_menu_list li a"} -end - diff --git a/features/step_definitions/additional_web_steps.rb b/features/step_definitions/additional_web_steps.rb index 6fd5dd8103e..66caa1d2f4b 100644 --- a/features/step_definitions/additional_web_steps.rb +++ b/features/step_definitions/additional_web_steps.rb @@ -1,81 +1,79 @@ -Then /^I should see a table header with "([^"]*)"$/ do |content| - expect(page).to 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| - expect(page).to_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| - expect(page).to 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| - expect(page).to_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| - trs = page.find(selector).all :css, 'tr' +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| - tds = page.find(selector).find('tr:first').all :css, 'td' +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| +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| - expect(page).to 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| - expect { +Then(/^an "([^"]*)" exception should be raised when I follow "([^"]*)"$/) do |error, link| + expect do step "I follow \"#{link}\"" - }.to raise_error(error.constantize) -end - -Then /^I should be in the resource section for (.+)$/ do |resource_name| - expect(current_url).to include resource_name.tr(' ', '').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 +Then(/^I should see the page title "([^"]*)"$/) do |title| + within("[data-test-page-header]") do expect(page).to have_content title end end -Then /^I should see a fieldset titled "([^"]*)"$/ do |title| - expect(page).to 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) - expect(field).to have_css 'option', text: option + expect(field).to have_css "option", text: option end -Then /^I should see the content "([^"]*)"$/ do |content| - expect(page).to 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| - expect(page).to 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| +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 7c1dd906987..00000000000 --- a/features/step_definitions/asset_steps.rb +++ /dev/null @@ -1,15 +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| - expect(page).to have_xpath("//link[contains(@href, '#{path}') and contains(@media, '#{media}')]", visible: false) -end - -Then /^I should see the js file "([^"]*)"$/ do |path| - expect(page).to have_xpath("//script[contains(@src, '#{path}')]", visible: false) -end - -Then /^I should see the favicon "([^"]*)"$/ do |path| - expect(page).to have_xpath("//link[contains(@href, '#{path}')]", visible: false) -end diff --git a/features/step_definitions/attribute_steps.rb b/features/step_definitions/attribute_steps.rb index 84dad126b10..20f652ba434 100644 --- a/features/step_definitions/attribute_steps.rb +++ b/features/step_definitions/attribute_steps.rb @@ -1,18 +1,19 @@ -Then /^I should see the attribute "([^"]*)" with "([^"]*)"$/ do |title, value| - elems = all ".attributes_table th:contains('#{title}') ~ td:contains('#{value}')" - expect(elems.first).to_not be_nil, 'attribute missing' -end +# 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}')" -Then /^I should see the attribute "([^"]*)" with a nicely formatted datetime$/ do |title| - text = all(".attributes_table th:contains('#{title}') ~ td").first.text - expect(text).to match /\w+ \d{1,2}, \d{4} \d{2}:\d{2}/ + if negate + expect(elems.first).to eq(nil), "attribute missing" + else + expect(elems.first).to_not eq(nil), "attribute missing" + end end -Then /^the attribute "([^"]*)" should be empty$/ do |title| - elems = all ".attributes_table th:contains('#{title}') ~ td > span.empty" - expect(elems.first).to_not be_nil, 'attribute not empty' +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| - expect(page).to_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 95a782caf63..2a0a126d4b3 100644 --- a/features/step_definitions/batch_action_steps.rb +++ b/features/step_definitions/batch_action_steps.rb @@ -1,81 +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' +# 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" - verb = maybe == 'should' ? :to : :to_not - expect(page).send verb, have_css(selector) -end - -Then /^I (should|should not) see the batch action :([^\s]*) "([^"]*)"$/ do |maybe, sym, title| - selector = ".batch_actions_selector a.batch_action" - selector << "[href='#'][data-action='#{sym}']" if maybe == 'should' - - verb = maybe == 'should' ? :to : :to_not + 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| - batch_action = page.all('.batch_actions_selector a.batch_action')[index.to_i - 1] +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 -end - -When /^I toggle the collection selection$/ do - page.find("#collection_selection_toggle_all").click +When(/^I check the (\d+)(?:st|nd|rd|th) record$/) do |index| + page.all(".batch-actions-resource-selection")[index.to_i].set true end -Then /^I should see that the batch action button is disabled$/ do - expect(page).to have_css ".batch_actions_selector .dropdown_menu_button.disabled" +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|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' +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" - verb = maybe == 'should' ? :to : :to_not + verb = maybe == "should" ? :to : :to_not expect(page).send verb, have_css(selector) end -Then /^I should see the batch action popover exists$/ do - expect(page).to 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| - # Use this implementation instead if poltergeist ever implements it - # page.driver.accept_modal(:confirm) { click_link(link) } - - click_link(link) - expect(page).to have_content("Are you sure you want to delete these posts?") - click_button("OK") -end - -Then /^I should not see checkboxes in the table$/ do - expect(page).to_not have_css '.paginated_collection table input[type=checkbox]' -end - -Then /^I should be show a input with name "([^"]*)" and type "([^"]*)"$/ do |name, type| - selector = ".batch_actions_selector a.batch_action:first" - expect(page.find(selector)["data-inputs"]).to eq "{\"#{name}\":\"#{type}\"}" +When(/^I click "(.*?)" and accept confirmation$/) do |link| + accept_confirm do + click_on(link) + end end -Then /^I should be show a select with name "([^"]*)" with the values "([^"]*)"$/ do |name, values| - selector = ".batch_actions_selector a.batch_action:first" - expect(JSON[page.find(selector)["data-inputs"]]).to eq Hash[name, values.split(', ')] +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/blog_steps.rb b/features/step_definitions/blog_steps.rb deleted file mode 100644 index d26cf415087..00000000000 --- a/features/step_definitions/blog_steps.rb +++ /dev/null @@ -1,3 +0,0 @@ -Then /^I should see a blog header "([^"]*)"$/ do |header_text| - expect(page).to have_css 'h3', text: header_text -end diff --git a/features/step_definitions/breadcrumb_steps.rb b/features/step_definitions/breadcrumb_steps.rb index 5c4c7da7cc7..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| - expect(page).to 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/column_steps.rb b/features/step_definitions/column_steps.rb deleted file mode 100644 index 0a47ba8f7a8..00000000000 --- a/features/step_definitions/column_steps.rb +++ /dev/null @@ -1,8 +0,0 @@ -Then /^I should see a columns container$/ do - expect(page).to have_css '.columns' -end - -Then /^I should see (a|\d+) columns?$/ do |count| - count = count == 'a' ? 1 : count.to_i - expect(page).to have_css '.column', count: count -end diff --git a/features/step_definitions/comment_steps.rb b/features/step_definitions/comment_steps.rb index 96b26e9d7ac..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 0e44b062369..69307e69ec5 100644 --- a/features/step_definitions/configuration_steps.rb +++ b/features/step_definitions/configuration_steps.rb @@ -1,100 +1,56 @@ +# frozen_string_literal: true module ActiveAdminReloading def load_aa_config(config_content) - ActiveSupport::Notifications.publish ActiveAdmin::Application::BeforeLoadEvent, ActiveAdmin.application + ActiveSupport::Notifications.instrument ActiveAdmin::Application::BeforeLoadEvent, { active_admin_application: ActiveAdmin.application } eval(config_content) - ActiveSupport::Notifications.publish ActiveAdmin::Application::AfterLoadEvent, ActiveAdmin.application + ActiveSupport::Notifications.instrument ActiveAdmin::Application::AfterLoadEvent, { active_admin_application: ActiveAdmin.application } Rails.application.reload_routes! - ActiveAdmin.application.namespaces.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' + when "index" + step "I am logged in" case resource = config_content.match(/ActiveAdmin\.register (\w+)/)[1] - when 'Post' - step 'I am on the index page for posts' - when 'Category' - step 'I am on the index page for categories' + 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 + # :nocov: raise "#{resource} is not supported" + # :nocov: end - when 'show' + 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' + 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 'Tag' - step 'I am logged in' + 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" Tag.create! visit admin_tag_path Tag.last else + # :nocov: raise "#{resource} is not supported" + # :nocov: 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 2a60a07f01d..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| - expect(page).to have_css '.dashboard .panel h3', text: name -end - -Then /^I should not see a dashboard widget "([^"]*)"$/ do |name| - expect(page).to_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 29523634b6f..43cf31f69ad 100644 --- a/features/step_definitions/factory_steps.rb +++ b/features/step_definitions/factory_steps.rb @@ -1,35 +1,46 @@ -def create_user(name, type = 'User') - first_name, last_name = name.split(' ') - user = type.camelize.constantize.where(first_name: first_name, last_name: last_name).first_or_create(username: name.tr(' ', '').underscore) -end - -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" +# 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)?( 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, custom_category_id: category.try(:id), starred: starred + 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| +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| +Given(/^a store named "([^"]*)" exists$/) do |name| Store.create! name: name end -Given /^I create a new post with the title "([^"]*)"$/ do |title| - first(:link, 'Posts').click - click_link "New Post" - fill_in 'post_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 02e91f20008..4ec225bf05d 100644 --- a/features/step_definitions/filter_steps.rb +++ b/features/step_definitions/filter_steps.rb @@ -1,16 +1,31 @@ -Then /^I should see a select filter for "([^"]*)"$/ do |label| - expect(page).to 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| - expect(page).to 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| - expect(page).to 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 @@ -18,22 +33,25 @@ 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 + separator = url.include?("?") ? "&" : "?" + visit url + separator + key.to_s + "=" + value.to_s end -Then(/^I should( not)? have parameter "([^"]*)"( with value "([^"]*)")?$/) do |negative, key, compare_val, value| +Then(/^I should have parameter "([^"]*)" with value "([^"]*)"$/) do |key, value| query = URI(page.current_url).query - if query.nil? - expect(negative).to be_truthy - else - params = Rack::Utils.parse_query query - if compare_val - expect(params[key]).to_not eq value if negative - expect(params[key]).to eq value unless negative - else - expect(params[key]).to be_nil if negative - expect(params[key]).to be_present unless negative - end - end + 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 d94c09e083d..00000000000 --- a/features/step_definitions/flash_steps.rb +++ /dev/null @@ -1,11 +0,0 @@ -Then /^I should see a flash with "([^"]*)"$/ do |text| - expect(page).to have_content text -end - -Then /^I should see a successful create flash$/ do - expect(page).to have_css 'div.flash_notice', text: /was successfully created/ -end - -Then /^I should not see a successful create flash$/ do - expect(page).to_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 15810260f80..26867d3d060 100644 --- a/features/step_definitions/format_steps.rb +++ b/features/step_definitions/format_steps.rb @@ -1,52 +1,67 @@ -require 'csv' +# 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 - expect(page.body).to match /\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| +Then(/^I should( not)? see a link to download "([^"]*)"$/) do |negate, format| method = negate ? :to_not : :to - expect(page).send method, have_css("#index_footer a", text: format) + 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 - content_type_header, content_disposition_header = %w[Content-Type Content-Disposition].map do |header_name| +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(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 = 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 be_nil - else - expect(cell || '').to 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| - expect(page.driver.response.body).to 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| +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_content(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 c45d9a489a7..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}"} - expect(page).to_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.tr(' ','').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.tr(" ", "").underscore.downcase - expect(page).to have_css ".scopes .#{name}" - expect(page).to_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 17df723e05c..00000000000 --- a/features/step_definitions/layout_steps.rb +++ /dev/null @@ -1,3 +0,0 @@ -Then /^I should see the Active Admin layout$/ do - expect(page).to 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 70bcc85b322..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| - expect(page).to have_css '.action_items a', text: content -end - -Then /^I should not see an action item button "([^"]*)"$/ do |content| - expect(page).to_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 361dc859ee4..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| - expect(page).to 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| - expect(page).to_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| - expect(page).to 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/meta_tag_steps.rb b/features/step_definitions/meta_tag_steps.rb deleted file mode 100644 index 0b41d04ea80..00000000000 --- a/features/step_definitions/meta_tag_steps.rb +++ /dev/null @@ -1,3 +0,0 @@ -Then /^the site should contain a meta tag with name "([^"]*)" and content "([^"]*)"$/ do |name, content| - expect(page).to have_xpath("//meta[@name='#{name}' and @content='#{content}']", visible: false) -end diff --git a/features/step_definitions/pagination_steps.rb b/features/step_definitions/pagination_steps.rb index d7c758e005b..33a15772da8 100644 --- a/features/step_definitions/pagination_steps.rb +++ b/features/step_definitions/pagination_steps.rb @@ -1,15 +1,16 @@ -Then /^I should not see pagination$/ do - expect(page).to_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| - expect(page).to have_css '.pagination span.page', count: count +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 "a", text: "Next" +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_not have_css "a", text: "Next" +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 2f345a80cae..00000000000 --- a/features/step_definitions/sidebar_steps.rb +++ /dev/null @@ -1,13 +0,0 @@ -Then /^I should see a sidebar titled "([^"]*)"$/ do |title| - expect(page).to have_css '.sidebar_section h3', text: title -end - -Then /^I should not see a sidebar titled "([^"]*)"$/ do |title| - title = title.tr(' ', '').underscore - sidebars = page.all :css, "##{title}_sidebar_section" - expect(sidebars.count).to eq 0 -end - -Then(/^I should see a sidebar titled "(.*?)" above sidebar titled "(.*?)"$/) do |top_title, bottom_title| - expect(page).to have_css %Q(.sidebar_section:contains('#{top_title}') + .sidebar_section:contains('#{bottom_title}')) -end diff --git a/features/step_definitions/site_title_steps.rb b/features/step_definitions/site_title_steps.rb index 9cc4eec21be..601281b5808 100644 --- a/features/step_definitions/site_title_steps.rb +++ b/features/step_definitions/site_title_steps.rb @@ -1,17 +1,14 @@ -Then /^I should see the site title "([^"]*)"$/ do |title| - expect(page).to have_css 'h1#site_title', text: title -end - -Then /^I should not see the site title "([^"]*)"$/ do |title| - expect(page).to_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| - img = page.find('h1#site_title img') - expect(img[:src]).to eq(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| - link = page.find('h1#site_title a') - expect(link[:href]).to eq(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/symbol_leak_steps.rb b/features/step_definitions/symbol_leak_steps.rb deleted file mode 100644 index d8ff1a60e37..00000000000 --- a/features/step_definitions/symbol_leak_steps.rb +++ /dev/null @@ -1,3 +0,0 @@ -Then /^"(.*?)" shouldn't be a symbol$/ do |sym| - expect(Symbol.all_symbols.map &:to_s).to_not include(sym), 'symbol detected!' -end diff --git a/features/step_definitions/tab_steps.rb b/features/step_definitions/tab_steps.rb deleted file mode 100644 index c94cc21ad92..00000000000 --- a/features/step_definitions/tab_steps.rb +++ /dev/null @@ -1,8 +0,0 @@ -Then /^the "([^"]*)" tab should be selected$/ do |name| - step %{I should see "#{name}" within "ul#tabs li.current"} -end - -Then(/^I should see two tabs "(.*?)" and "(.*?)"$/) do |tab_1, tab_2| - expect(page).to have_link(tab_1) - expect(page).to have_link(tab_2) -end diff --git a/features/step_definitions/table_steps.rb b/features/step_definitions/table_steps.rb index 236241816ca..29a979a2338 100644 --- a/features/step_definitions/table_steps.rb +++ b/features/step_definitions/table_steps.rb @@ -1,6 +1,18 @@ -Then /^I should see (\d+) ([\w]*) in the table$/ do |count, resource_type| - expect(page).to \ - have_css("table.index_table tr > td:first-child", count: 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? @@ -13,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 @@ -23,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 + # :nocov: raise "I don't know what to do with #{input}" + # :nocov: 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 @@ -79,30 +75,30 @@ def assert_tables_match(table, expected_table) begin assert_cells_match(cell, expected_cell) rescue + # :nocov: 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 $! + # :nocov: end end end end def assert_cells_match(cell, expected_cell) - if expected_cell =~ /^\/.*\/$/ - expect(cell).to match /#{expected_cell[1..-2]}/ + if /^\/.*\/$/.match?(expected_cell) + expect(cell).to match(/#{expected_cell[1..-2]}/) else expect((cell || "").strip).to eq expected_cell end end - -end # module TableMatchHelper +end World(TableMatchHelper) - # Usage: # # I should see the "invoices" table: @@ -110,7 +106,7 @@ 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| +Then(/^I should see the "([^"]*)" table:$/) do |table_id, expected_table| expect(page).to have_css "table##{table_id}" assert_tables_match( diff --git a/features/step_definitions/user_steps.rb b/features/step_definitions/user_steps.rb index 880acaaa548..f5834d27d20 100644 --- a/features/step_definitions/user_steps.rb +++ b/features/step_definitions/user_steps.rb @@ -1,45 +1,47 @@ +# frozen_string_literal: true def ensure_user_created(email) - user = AdminUser.where(email: email).first_or_create(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(:user) + login_as ensure_user_created "admin@example.com" 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 /^"([^"]*)" requests a password reset with token "([^"]*)"( but it expires)?$/ do |email, token, expired| +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 + fill_in "Email", with: email allow(Devise).to receive(:friendly_token).and_return(token) - click_button "Reset My Password" + click_on "Reset My Password" AdminUser.where(email: email).first.update_attribute :reset_password_sent_at, 1.month.ago if expired end -When /^I fill in the password field with "([^"]*)"$/ do |password| - fill_in 'admin_user_password', with: password +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 0626344f2bb..ae59d96488b 100644 --- a/features/step_definitions/web_steps.rb +++ b/features/step_definitions/web_steps.rb @@ -1,85 +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 + # :nocov: + raise "Can't find mapping from \"#{locator}\" to a selector.\n" + + "Now, go and add a mapping in #{__FILE__}" + # :nocov: + 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 } -end - -Given /^(?:I )am on (.+)$/ do |page_name| +Given(/^I am on (.+)$/) do |page_name| visit path_to(page_name) end -When /^(?:I )go to (.+)$/ do |page_name| +When(/^I go to (.+)$/) do |page_name| visit path_to(page_name) end -When /^(?:I )POST to "(.*?)" with params "(.*?)"$/ do |url, params| - post "#{url}#{params}" +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| +When(/^I follow "([^"]*)"$/) do |link| first(:link, link).click end -When /^(?:I )fill in "([^"]*)" with "([^"]*)"$/ do |field, value| +When(/^I click "(.*?)"$/) do |link| + click_on(link) +end + +When(/^I fill in "([^"]*)" with "([^"]*)"$/) do |field, value| fill_in(field, with: value) end -When /^(?:I )select "([^"]*)" from "([^"]*)"$/ do |value, field| +When(/^I select "([^"]*)" from "([^"]*)"$/) do |value, field| select(value, from: field) end -When /^(?:I )(check|uncheck|choose) "([^"]*)"$/ do |action, field| +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_content(text) + expect(page).send should, have end -Then /^(?:I )should( not)? see( the element)? "([^"]*)"$/ do |negate, is_css, text| - should = negate ? :not_to : :to - have = is_css ? have_css(text) : have_content(text) - expect(page).send should, have +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 /^the "([^"]*)" field(?: within (.*))? should( not)? contain "([^"]*)"$/ do |field, parent, negate, value| +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 contain "([^"]*)"$/) do |field, parent, value| with_scope(parent) do field = find_field(field) - value = field.tag_name == 'textarea' ? field.text : field.value + value = field.tag_name == "textarea" ? field.text : field.value + + expect(value).to match(/#{value}/) + end +end - expect(value).send negate ? :not_to : :to, match(/#{value}/) +Then(/^the "([^"]*)" select(?: within (.*))? should have "([^"]+)" selected$/) do |label, parent, option| + with_scope(parent) do + expect(page).to have_select(label, selected: option) end end -Then /^the "([^"]*)" checkbox(?: within (.*))? should( not)? be checked$/ do |label, parent, negate| +Then(/^the "([^"]*)" checkbox(?: within (.*))? should( not)? be checked$/) do |label, parent, negate| with_scope(parent) do - expect(find_field(label)['checked']).to negate ? eq(false) : eq(true) + 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| +Then(/^I should be on (.+)$/) do |page_name| expect(URI.parse(current_url).path).to eq path_to page_name end -Then /^show me the page$/ do - save_and_open_page +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(/^I should see a flash with "([^"]*)"$/) do |text| + expect(page).to have_content text end diff --git a/features/sti_resource.feature b/features/sti_resource.feature index e809ccb9229..54d0929c442 100644 --- a/features/sti_resource.feature +++ b/features/sti_resource.feature @@ -7,10 +7,10 @@ Feature: STI Resource And a configuration of: """ ActiveAdmin.register Publisher do - permit_params :first_name, :last_name, :username, :age if Rails::VERSION::MAJOR == 4 + 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 if Rails::VERSION::MAJOR == 4 + permit_params :first_name, :last_name, :username, :age, :reason_of_sign_in end """ diff --git a/features/strong_parameters.feature b/features/strong_parameters.feature index 807e2fbc8be..51e527cf28b 100644 --- a/features/strong_parameters.feature +++ b/features/strong_parameters.feature @@ -1,21 +1,17 @@ -@rails4 @silent_unpermitted_params_failure Feature: Strong Params - When I am using Rails 4 - I want to use Strong Parameters - 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 - Given a configuration of: + And a configuration of: """ ActiveAdmin.register Post do end """ - When I am on the index page for posts + And I am on the index page for posts Scenario: Static permitted parameters Given a configuration of: @@ -24,15 +20,15 @@ Feature: Strong Params permit_params :author, :title, :starred end """ - Given I follow "Edit" + When I follow "Edit" - When I fill in "Title" with "Hello World from update" + And I fill in "Title" with "Hello World from update" And I check "Starred" - When I press "Update Post" + 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 "true" + And I should see the attribute "Starred" with "Yes" Scenario: Dynamic permitted parameters Given a configuration of: @@ -45,15 +41,15 @@ Feature: Strong Params end end """ - Given I follow "Edit" + When I follow "Edit" - When I fill in "Title" with "Hello World from update" + And I fill in "Title" with "Hello World from update" And I check "Starred" - When I press "Update Post" + 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 "true" + And I should see the attribute "Starred" with "Yes" Scenario: Should not update parameters that are not declared as permitted Given a configuration of: @@ -62,12 +58,12 @@ Feature: Strong Params permit_params :author, :title end """ - Given I follow "Edit" + When I follow "Edit" - When I fill in "Title" with "Hello World from update" + And I fill in "Title" with "Hello World from update" And I check "Starred" - When I press "Update Post" + 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 the attribute "Starred" should be empty + And I should see the attribute "Starred" with "Unknown" diff --git a/features/support/env.rb b/features/support/env.rb index 8ad400dcebd..26780e50778 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -1,42 +1,27 @@ -# 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" +require "simplecov" if ENV["COVERAGE"] == "true" -require File.expand_path('../../../spec/spec_helper', __FILE__) - -ENV['RAILS_ROOT'] = File.expand_path("../../../spec/rails/rails-#{ENV["RAILS"]}", __FILE__) - -# Create the test app if it doesn't exists -unless File.exists?(ENV['RAILS_ROOT']) - system 'rake setup' +Dir["#{File.expand_path('../step_definitions', __dir__)}/*.rb"].each do |f| + require f end -require 'rails' -require 'active_record' -require 'active_admin' -require 'devise' -ActiveAdmin.application.load_paths = [ENV['RAILS_ROOT'] + "/app/admin"] +require_relative "../../tasks/test_application" -require ENV['RAILS_ROOT'] + '/config/environment' +require "#{ActiveAdmin::TestApplication.new.full_app_dir}/config/environment.rb" -# Setup autoloading of ActiveAdmin and the load path -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -autoload :ActiveAdmin, 'active_admin' +require_relative "rails" +require_relative "../../spec/support/active_support_deprecation" -require 'cucumber/rails' - -require 'rspec/mocks' +require "rspec/mocks" World(RSpec::Mocks::ExampleMethods) -Before do +Around "@mocks" do |scenario, block| RSpec::Mocks.setup -end -After do + block.call + begin RSpec::Mocks.verify ensure @@ -44,13 +29,23 @@ end end -require 'capybara/rails' -require 'capybara/cucumber' -require 'capybara/session' -require 'capybara/poltergeist' -require 'phantomjs/poltergeist' +After "@debug" do |scenario| + # :nocov: + save_and_open_page if scenario.failed? + # :nocov: +end + +require "capybara/cuprite" + +Capybara.register_driver(:cuprite) do |app| + Capybara::Cuprite::Driver.new(app, process_timeout: 30, timeout: 30) +end -Capybara.javascript_driver = :poltergeist +Capybara.javascript_driver = :cuprite + +Capybara.server = :webrick + +Capybara.asset_host = "http://localhost:3000" # 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 @@ -58,88 +53,58 @@ # 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 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 - -# 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 } +Around "@locale_manipulation" do |scenario, block| + I18n.with_locale(:en, &block) end -# Don't run @rails4 tagged features for versions before Rails 4. -Before('@rails4') do |scenario| - scenario.skip_invoke! if Rails::VERSION::MAJOR < 4 -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 -Around '@silent_unpermitted_params_failure' do |scenario, block| - original = ActionController::Parameters.action_on_unpermitted_parameters - ActionController::Parameters.action_on_unpermitted_parameters = false - block.call - ActionController::Parameters.action_on_unpermitted_parameters = original + def self.index_name + "custom" + end end diff --git a/features/support/paths.rb b/features/support/paths.rb index 325d902ebd9..92e6e75ccc8 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 # @@ -6,38 +7,31 @@ module NavigationHelpers # step definition in web_steps.rb # def path_to(page_name) - params = page_name.scan(/with params "(.*?)"/).flatten[0] || '' - page_name.sub! /\ with params.*/, '' + case page_name - url = case page_name - - when /the home\s?page/ - '/' when /the dashboard/ "/admin" when /the new post page/ "/admin/posts/new" when /the login page/ "/admin/login" - when /the first post show page/ - "/admin/posts/1" - when /the first post edit page/ - "/admin/posts/1/edit" + 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) @@ -45,9 +39,18 @@ def path_to(page_name) when /^the last author's last post page$/ 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: # @@ -58,13 +61,14 @@ def path_to(page_name) begin page_name =~ /the (.*) page/ path_components = $1.split(/\s+/) - self.send path_components.push('path').join('_') - rescue Object => e + self.send path_components.push("path").join("_") + # :nocov: + rescue Object raise "Can't find mapping from \"#{page_name}\" to a path.\n" + "Now, go and add a mapping in #{__FILE__}" + # :nocov: end end - url + params end end diff --git a/features/support/rails.rb b/features/support/rails.rb new file mode 100644 index 00000000000..dd430286225 --- /dev/null +++ b/features/support/rails.rb @@ -0,0 +1,14 @@ +# 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/strategy" +require "cucumber/rails/database/deletion_strategy" +require "cucumber/rails/database/null_strategy" +require "cucumber/rails/database/shared_connection_strategy" +require "cucumber/rails/database/truncation_strategy" +require "cucumber/rails/database" + +MultiTest.disable_autorun diff --git a/features/support/selectors.rb b/features/support/selectors.rb deleted file mode 100644 index 26526291079..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.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 - 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..d653328e7b3 --- /dev/null +++ b/features/support/simplecov_changes_env.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +if ENV["COVERAGE"] == "true" + require "simplecov" + + SimpleCov.command_name "filesystem changes features" +end + +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..6d9c87e22f3 --- /dev/null +++ b/features/support/simplecov_regular_env.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +if ENV["COVERAGE"] == "true" + require "simplecov" + + SimpleCov.command_name ["regular features", ENV["TEST_ENV_NUMBER"]].join(" ").rstrip +end + +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..db5499c42b6 --- /dev/null +++ b/features/support/simplecov_reload_env.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +if ENV["COVERAGE"] == "true" + require "simplecov" + + SimpleCov.command_name "reload features" +end + +require_relative "env" diff --git a/features/symbol_leak.feature b/features/symbol_leak.feature deleted file mode 100644 index 6165103639b..00000000000 --- a/features/symbol_leak.feature +++ /dev/null @@ -1,35 +0,0 @@ -Feature: User input shouldn't be symbolized - - Background: - Given a configuration of: - """ - ActiveAdmin.register Post - """ - Given I am logged in - And 1 post exists - - Scenario: The dashboard doesn't leak - Given I am on the dashboard with params "?really_long_malicious_key0" - Then "really_long_malicious_key0" shouldn't be a symbol - - Scenario: The index page doesn't leak - Given I am on the index page for posts with params "?really_long_malicious_key1" - Then "really_long_malicious_key1" shouldn't be a symbol - - @allow-rescue - Scenario: The filter query hash doesn't leak - Given I am on the index page for posts with params "?q[really_long_malicious_key2]" - Then "really_long_malicious_key2" shouldn't be a symbol - - Scenario: The show page doesn't leak - Given I go to the first post show page with params "?really_long_malicious_key3" - Then "really_long_malicious_key3" shouldn't be a symbol - - Scenario: The edit page doesn't leak - Given I go to the first post edit page with params "?really_long_malicious_key4" - Then "really_long_malicious_key4" shouldn't be a symbol - - @allow-rescue - Scenario: Batch Actions don't leak - Given I POST to "admin/posts/batch_action" with params "?batch_action=really_long_malicious_key5" - Then "really_long_malicious_key5" shouldn't be a symbol diff --git a/features/users/logging_in.feature b/features/users/logging_in.feature index f347575a775..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 the element "a[href='/admin/logout' ]:contains('Logout')" + 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 262f500566e..dbad1f8e3de 100644 --- a/features/users/resetting_password.feature +++ b/features/users/resetting_password.feature @@ -3,31 +3,29 @@ 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 on how to reset your password in a few minutes." + @mocks Scenario: Changing password after resetting When "admin@example.com" requests a password reset with token "foobarbaz" - When I go to the admin password reset form 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 When "admin@example.com" requests a password reset with token "foobarbaz" but it expires - When I go to the admin password reset form 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 "wrong" And I press "Change my password" diff --git a/gemfiles/rails_72/Gemfile b/gemfiles/rails_72/Gemfile new file mode 100644 index 00000000000..910fb63acf3 --- /dev/null +++ b/gemfiles/rails_72/Gemfile @@ -0,0 +1,44 @@ +# frozen_string_literal: true +source "https://rubygems.org" + +group :development, :test do + gem "rake" + + gem "cancancan" + gem "pundit" + + gem "draper" + gem "devise", "~> 4.9" # TODO: relax this dependency when formtastic/formtastic#1401 will be fixed + + gem "rails", "~> 7.2.0" + + gem "sprockets-rails" + gem "ransack", ">= 4.2.0" + gem "formtastic", ">= 5.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 "cucumber" + 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..be577b6d979 --- /dev/null +++ b/gemfiles/rails_72/Gemfile.lock @@ -0,0 +1,464 @@ +PATH + remote: ../.. + specs: + activeadmin (4.0.0.beta21) + arbre (~> 2.0) + csv + formtastic (>= 5.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.3) + actionpack (= 7.2.3) + activesupport (= 7.2.3) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (7.2.3) + actionpack (= 7.2.3) + activejob (= 7.2.3) + activerecord (= 7.2.3) + activestorage (= 7.2.3) + activesupport (= 7.2.3) + mail (>= 2.8.0) + actionmailer (7.2.3) + actionpack (= 7.2.3) + actionview (= 7.2.3) + activejob (= 7.2.3) + activesupport (= 7.2.3) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (7.2.3) + actionview (= 7.2.3) + activesupport (= 7.2.3) + 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.3) + actionpack (= 7.2.3) + activerecord (= 7.2.3) + activestorage (= 7.2.3) + activesupport (= 7.2.3) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.2.3) + activesupport (= 7.2.3) + builder (~> 3.1) + cgi + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.2.3) + activesupport (= 7.2.3) + globalid (>= 0.3.6) + activemodel (7.2.3) + activesupport (= 7.2.3) + activemodel-serializers-xml (1.0.3) + activemodel (>= 5.0.0.a) + activesupport (>= 5.0.0.a) + builder (~> 3.1) + activerecord (7.2.3) + activemodel (= 7.2.3) + activesupport (= 7.2.3) + timeout (>= 0.4.0) + activestorage (7.2.3) + actionpack (= 7.2.3) + activejob (= 7.2.3) + activerecord (= 7.2.3) + activesupport (= 7.2.3) + marcel (~> 1.0) + activesupport (7.2.3) + 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) + addressable (2.8.8) + 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.21) + benchmark (0.5.0) + bigdecimal (4.0.1) + 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.1) + childprocess (5.1.0) + logger (~> 1.5) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + crass (1.0.6) + cssbundling-rails (1.4.3) + railties (>= 6.0.0) + csv (3.3.5) + cucumber (10.2.0) + base64 (~> 0.2) + builder (~> 3.2) + cucumber-ci-environment (> 9, < 12) + cucumber-core (> 15, < 17) + cucumber-cucumber-expressions (> 17, < 20) + cucumber-html-formatter (> 21, < 23) + diff-lcs (~> 1.5) + logger (~> 1.6) + mini_mime (~> 1.1) + multi_test (~> 1.1) + sys-uname (~> 1.3) + cucumber-ci-environment (11.0.0) + cucumber-core (16.1.1) + cucumber-gherkin (> 36, < 40) + cucumber-messages (> 31, < 33) + cucumber-tag-expressions (> 6, < 9) + cucumber-cucumber-expressions (18.1.0) + bigdecimal + cucumber-gherkin (38.0.0) + cucumber-messages (>= 31, < 33) + cucumber-html-formatter (22.3.0) + cucumber-messages (> 23, < 33) + cucumber-messages (32.0.1) + cucumber-rails (4.0.0) + capybara (>= 3.25, < 4) + cucumber (>= 7, < 11) + railties (>= 6.1, < 9) + cucumber-tag-expressions (8.1.0) + cuprite (0.17) + capybara (~> 3.0) + ferrum (~> 0.17.0) + database_cleaner-active_record (2.2.2) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0) + database_cleaner-core (2.0.1) + date (3.5.1) + devise (4.9.4) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) + diff-lcs (1.6.2) + docile (1.4.1) + 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.1) + erubi (1.13.1) + ferrum (0.17.1) + addressable (~> 2.5) + base64 (~> 0.2) + concurrent-ruby (~> 1.1) + webrick (~> 1.7) + websocket-driver (~> 0.7) + ffi (1.17.3) + ffi (1.17.3-aarch64-linux-gnu) + ffi (1.17.3-arm64-darwin) + ffi (1.17.3-x86_64-darwin) + ffi (1.17.3-x86_64-linux-gnu) + formtastic (5.0.0) + actionpack (>= 6.0.0) + formtastic_i18n (0.7.0) + globalid (1.3.0) + activesupport (>= 6.1) + has_scope (0.9.0) + actionpack (>= 7.0) + activesupport (>= 7.0) + highline (3.1.2) + reline + i18n (1.14.8) + 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.8.2) + irb (1.16.0) + pp (>= 0.6.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.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + matrix (0.4.3) + memoist3 (1.0.0) + mini_mime (1.1.5) + minitest (6.0.1) + prism (~> 1.5) + multi_test (1.1.0) + net-imap (0.6.2) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.0-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.0-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.0-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.0-x86_64-linux-gnu) + racc (~> 1.4) + orm_adapter (0.5.0) + parallel (1.27.0) + parallel_tests (5.5.0) + parallel + parser (3.3.10.1) + ast (~> 2.4.1) + racc + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.8.0) + psych (5.3.1) + date + stringio + public_suffix (7.0.2) + pundit (2.5.2) + activesupport (>= 3.0.0) + racc (1.8.1) + rack (3.2.4) + rack-session (2.1.1) + 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.3) + actioncable (= 7.2.3) + actionmailbox (= 7.2.3) + actionmailer (= 7.2.3) + actionpack (= 7.2.3) + actiontext (= 7.2.3) + actionview (= 7.2.3) + activejob (= 7.2.3) + activemodel (= 7.2.3) + activerecord (= 7.2.3) + activestorage (= 7.2.3) + activesupport (= 7.2.3) + bundler (>= 1.15.0) + railties (= 7.2.3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + 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.3) + actionpack (= 7.2.3) + activesupport (= 7.2.3) + 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.3.1) + ransack (4.4.1) + activerecord (>= 7.2) + activesupport (>= 7.2) + i18n + rdoc (7.1.0) + erb + psych (>= 4.0.0) + tsort + regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) + request_store (1.7.0) + rack (>= 1.4) + responders (3.2.0) + 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.7) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.2) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.6) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + securerandom (0.4.1) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-cobertura (3.1.0) + rexml + simplecov (~> 0.19) + simplecov-html (0.13.2) + simplecov_json_formatter (0.1.4) + sprockets (4.2.2) + concurrent-ruby (~> 1.0) + logger + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + sprockets (>= 3.0.0) + sqlite3 (2.9.0-aarch64-linux-gnu) + sqlite3 (2.9.0-arm64-darwin) + sqlite3 (2.9.0-x86_64-darwin) + sqlite3 (2.9.0-x86_64-linux-gnu) + stringio (3.2.0) + sys-uname (1.4.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.0) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + useragent (0.16.11) + warden (1.2.9) + rack (>= 2.0.9) + webrick (1.9.2) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.7.4) + +PLATFORMS + aarch64-linux + arm64-darwin + x86_64-darwin + x86_64-linux + +DEPENDENCIES + activeadmin! + cancancan + capybara + cssbundling-rails + cucumber + cucumber-rails + cuprite + database_cleaner-active_record + devise (~> 4.9) + draper + formtastic (>= 5.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.4 diff --git a/gemfiles/rails_80/Gemfile b/gemfiles/rails_80/Gemfile new file mode 100644 index 00000000000..2f3af28655c --- /dev/null +++ b/gemfiles/rails_80/Gemfile @@ -0,0 +1,44 @@ +# frozen_string_literal: true +source "https://rubygems.org" + +group :development, :test do + gem "rake" + + gem "cancancan" + gem "pundit" + + gem "draper" + gem "devise", "~> 4.9" # TODO: relax this dependency when formtastic/formtastic#1401 will be fixed + + gem "rails", "~> 8.0.0" + + gem "sprockets-rails" + gem "ransack", ">= 4.2.0" + gem "formtastic", ">= 5.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 "cucumber" + 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..821964a7b5c --- /dev/null +++ b/gemfiles/rails_80/Gemfile.lock @@ -0,0 +1,461 @@ +PATH + remote: ../.. + specs: + activeadmin (4.0.0.beta21) + arbre (~> 2.0) + csv + formtastic (>= 5.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.4) + actionpack (= 8.0.4) + activesupport (= 8.0.4) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.0.4) + actionpack (= 8.0.4) + activejob (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) + mail (>= 2.8.0) + actionmailer (8.0.4) + actionpack (= 8.0.4) + actionview (= 8.0.4) + activejob (= 8.0.4) + activesupport (= 8.0.4) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.0.4) + actionview (= 8.0.4) + activesupport (= 8.0.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.0.4) + actionpack (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.0.4) + activesupport (= 8.0.4) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.0.4) + activesupport (= 8.0.4) + globalid (>= 0.3.6) + activemodel (8.0.4) + activesupport (= 8.0.4) + activemodel-serializers-xml (1.0.3) + activemodel (>= 5.0.0.a) + activesupport (>= 5.0.0.a) + builder (~> 3.1) + activerecord (8.0.4) + activemodel (= 8.0.4) + activesupport (= 8.0.4) + timeout (>= 0.4.0) + activestorage (8.0.4) + actionpack (= 8.0.4) + activejob (= 8.0.4) + activerecord (= 8.0.4) + activesupport (= 8.0.4) + marcel (~> 1.0) + activesupport (8.0.4) + 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.8.8) + 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.21) + benchmark (0.5.0) + bigdecimal (4.0.1) + 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.6) + connection_pool (3.0.2) + crass (1.0.6) + cssbundling-rails (1.4.3) + railties (>= 6.0.0) + csv (3.3.5) + cucumber (10.2.0) + base64 (~> 0.2) + builder (~> 3.2) + cucumber-ci-environment (> 9, < 12) + cucumber-core (> 15, < 17) + cucumber-cucumber-expressions (> 17, < 20) + cucumber-html-formatter (> 21, < 23) + diff-lcs (~> 1.5) + logger (~> 1.6) + mini_mime (~> 1.1) + multi_test (~> 1.1) + sys-uname (~> 1.3) + cucumber-ci-environment (11.0.0) + cucumber-core (16.1.1) + cucumber-gherkin (> 36, < 40) + cucumber-messages (> 31, < 33) + cucumber-tag-expressions (> 6, < 9) + cucumber-cucumber-expressions (18.1.0) + bigdecimal + cucumber-gherkin (38.0.0) + cucumber-messages (>= 31, < 33) + cucumber-html-formatter (22.3.0) + cucumber-messages (> 23, < 33) + cucumber-messages (32.0.1) + cucumber-rails (4.0.0) + capybara (>= 3.25, < 4) + cucumber (>= 7, < 11) + railties (>= 6.1, < 9) + cucumber-tag-expressions (8.1.0) + cuprite (0.17) + capybara (~> 3.0) + ferrum (~> 0.17.0) + database_cleaner-active_record (2.2.2) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0) + database_cleaner-core (2.0.1) + date (3.5.1) + devise (4.9.4) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) + diff-lcs (1.6.2) + docile (1.4.1) + 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.1) + erubi (1.13.1) + ferrum (0.17.1) + addressable (~> 2.5) + base64 (~> 0.2) + concurrent-ruby (~> 1.1) + webrick (~> 1.7) + websocket-driver (~> 0.7) + ffi (1.17.3) + ffi (1.17.3-aarch64-linux-gnu) + ffi (1.17.3-arm64-darwin) + ffi (1.17.3-x86_64-darwin) + ffi (1.17.3-x86_64-linux-gnu) + formtastic (5.0.0) + actionpack (>= 6.0.0) + formtastic_i18n (0.7.0) + globalid (1.3.0) + activesupport (>= 6.1) + has_scope (0.9.0) + actionpack (>= 7.0) + activesupport (>= 7.0) + highline (3.1.2) + reline + i18n (1.14.8) + 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.8.2) + irb (1.16.0) + pp (>= 0.6.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.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + matrix (0.4.3) + memoist3 (1.0.0) + mini_mime (1.1.5) + minitest (6.0.1) + prism (~> 1.5) + multi_test (1.1.0) + net-imap (0.6.2) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.0-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.0-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.0-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.0-x86_64-linux-gnu) + racc (~> 1.4) + orm_adapter (0.5.0) + parallel (1.27.0) + parallel_tests (5.5.0) + parallel + parser (3.3.10.1) + ast (~> 2.4.1) + racc + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.8.0) + psych (5.3.1) + date + stringio + public_suffix (7.0.2) + pundit (2.5.2) + activesupport (>= 3.0.0) + racc (1.8.1) + rack (3.2.4) + rack-session (2.1.1) + 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.4) + actioncable (= 8.0.4) + actionmailbox (= 8.0.4) + actionmailer (= 8.0.4) + actionpack (= 8.0.4) + actiontext (= 8.0.4) + actionview (= 8.0.4) + activejob (= 8.0.4) + activemodel (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) + bundler (>= 1.15.0) + railties (= 8.0.4) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + 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.4) + actionpack (= 8.0.4) + activesupport (= 8.0.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.3.1) + ransack (4.4.1) + activerecord (>= 7.2) + activesupport (>= 7.2) + i18n + rdoc (7.1.0) + erb + psych (>= 4.0.0) + tsort + regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) + request_store (1.7.0) + rack (>= 1.4) + responders (3.2.0) + 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.7) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.2) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.6) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + securerandom (0.4.1) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-cobertura (3.1.0) + rexml + simplecov (~> 0.19) + simplecov-html (0.13.2) + simplecov_json_formatter (0.1.4) + sprockets (4.2.2) + concurrent-ruby (~> 1.0) + logger + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + sprockets (>= 3.0.0) + sqlite3 (2.9.0-aarch64-linux-gnu) + sqlite3 (2.9.0-arm64-darwin) + sqlite3 (2.9.0-x86_64-darwin) + sqlite3 (2.9.0-x86_64-linux-gnu) + stringio (3.2.0) + sys-uname (1.4.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.0) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.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.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.7.4) + +PLATFORMS + aarch64-linux + arm64-darwin + x86_64-darwin + x86_64-linux + +DEPENDENCIES + activeadmin! + cancancan + capybara + cssbundling-rails + cucumber + cucumber-rails + cuprite + database_cleaner-active_record + devise (~> 4.9) + draper + formtastic (>= 5.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.4 diff --git a/lib/active_admin.rb b/lib/active_admin.rb index 5f14f179819..148f34468e2 100644 --- a/lib/active_admin.rb +++ b/lib/active_admin.rb @@ -1,70 +1,71 @@ -require 'active_support/core_ext' -require 'set' - -require 'ransack' -require 'ransack_ext' -require 'bourbon' -require 'kaminari' -require 'formtastic' -require 'formtastic_i18n' -require 'sass-rails' -require 'inherited_resources' -require 'jquery-rails' -require 'jquery-ui-rails' -require 'coffee-rails' -require 'arbre' - -require 'active_admin/helpers/i18n' +# 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 :Callbacks, 'active_admin/callbacks' - autoload :Component, 'active_admin/component' - autoload :BaseController, 'active_admin/base_controller' - autoload :CanCanAdapter, 'active_admin/cancan_adapter' - autoload :ControllerAction, 'active_admin/controller_action' - autoload :CSVBuilder, 'active_admin/csv_builder' - autoload :Dependency, 'active_admin/dependency' - 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 :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 :PageController, 'active_admin/page_controller' - autoload :PageDSL, 'active_admin/page_dsl' - autoload :PunditAdapter, 'active_admin/pundit_adapter' - 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! @@ -72,11 +73,11 @@ def setup application.prepare! end - delegate :register, to: :application + delegate :register, to: :application delegate :register_page, to: :application - delegate :unload!, to: :application - delegate :load!, to: :application - delegate :routes, 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 @@ -92,7 +93,7 @@ def setup # # @param [Block] block A block to call each time (before) AA loads resources def before_load(&block) - ActiveSupport::Notifications.subscribe ActiveAdmin::Application::BeforeLoadEvent, &ActiveAdmin::Event.wrap_block_for_active_support_notifications(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 @@ -110,7 +111,13 @@ def before_load(&block) # # @param [Block] block A block to call each time (after) AA loads resources def after_load(&block) - ActiveSupport::Notifications.subscribe ActiveAdmin::Application::AfterLoadEvent, &ActiveAdmin::Event.wrap_block_for_active_support_notifications(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 @@ -118,13 +125,12 @@ def after_load(&block) end # Require things that don't support autoload -require 'active_admin/engine' -require 'active_admin/error' +require_relative "active_admin/engine" +require_relative "active_admin/error" # Require internal plugins -require 'active_admin/batch_actions' -require 'active_admin/filters' +require_relative "active_admin/batch_actions" +require_relative "active_admin/filters" # Require ORM-specific plugins -require 'active_admin/orm/active_record' if defined? ActiveRecord -require 'active_admin/orm/mongoid' if defined? Mongoid +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 8b35cbe61ed..00000000000 --- a/lib/active_admin/abstract_view_factory.rb +++ /dev/null @@ -1,85 +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 - - def respond_to_missing?(method, include_private) - 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?('=') - set_view_for_key key, args.first - else - get_view_for_key key - end - else - super - end - end - - def key_from_method_name(method) - method.to_s.tr('=', '').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 54200e82cff..8be729c29bc 100644 --- a/lib/active_admin/application.rb +++ b/lib/active_admin/application.rb @@ -1,145 +1,54 @@ -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 = Namespace::Store.new + 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 max number of resources to display on index pages and batch exports - inheritable_setting :max_per_page, 10_000 - - # 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, "" - - # Set a favicon - inheritable_setting :favicon, false - - # Additional meta tags to place in head of logged in pages. - inheritable_setting :meta_tags, {} - - # Additional meta tags to place in head of logged out pages. - inheritable_setting :meta_tags_for_logged_out_pages, { robots: "noindex, nofollow" } - - # 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' - - # Options that a passed to root_to. - inheritable_setting :root_to_options, {} - - # Display breadcrumbs - inheritable_setting :breadcrumb, true - - # Default CSV options - inheritable_setting :csv_options, { col_sep: ',', byte_order_mark: "\xEF\xBB\xBF" } - - # 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 - - # A regex to detect unsupported browser, set to false to disable - inheritable_setting :unsupported_browser_matcher, /MSIE [1-8]\.0/ - - # Whether to display 'Current Filters' on search screen - inheritable_setting :current_filters, true - - # Request parameters that are permitted by default - inheritable_setting :permitted_params, [ - :utf8, :_method, :authenticity_token, :commit, :id - ] - - # Set flash message keys that shouldn't show in ActiveAdmin - inheritable_setting :flash_keys_to_except, ['timedout'] - - # Set default localize format for Date/Time values - inheritable_setting :localize_format, :long - - # Include association filters by default - inheritable_setting :include_default_association_filters, true + 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 - def allow_comments=(*) - raise "`config.allow_comments` is no longer provided in ActiveAdmin 1.x. Use `config.comments` instead." + 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 @@ -150,7 +59,7 @@ def prepare! # Registers a brand new configuration for the given resource. def register(resource, options = {}, &block) - ns = options.fetch(:namespace){ default_namespace } + ns = options.fetch(:namespace) { default_namespace } namespace(ns).register resource, options, &block end @@ -162,9 +71,9 @@ def register(resource, options = {}, &block) def namespace(name) name ||= :root - namespace = namespaces[name] ||= begin + namespace = namespaces[name.to_sym] ||= begin namespace = Namespace.new(self, name) - ActiveSupport::Notifications.publish ActiveAdmin::Namespace::RegisterEvent, namespace + ActiveSupport::Notifications.instrument ActiveAdmin::Namespace::RegisterEvent, { active_admin_namespace: namespace } namespace end @@ -180,7 +89,7 @@ def namespace(name) # @&block The registration block. # def register_page(name, options = {}, &block) - ns = options.fetch(:namespace){ default_namespace } + ns = options.fetch(:namespace) { default_namespace } namespace(ns).register_page name, options, &block end @@ -192,7 +101,7 @@ def loaded? # Removes all defined controllers from memory. Useful in # development, where they are reloaded on each request. def unload! - namespaces.each &:unload! + namespaces.each(&:unload!) @@loaded = false end @@ -200,60 +109,57 @@ def unload! # To reload everything simply call `ActiveAdmin.unload!` def load! unless loaded? - ActiveSupport::Notifications.publish BeforeLoadEvent, self # before_load hook - files.each{ |file| load file } # load files - namespace(default_namespace) # init AA resources - ActiveSupport::Notifications.publish 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 def load(file) - DatabaseHitDuringLoad.capture{ super } + DatabaseHitDuringLoad.capture { super } end # Returns ALL the files to be loaded def files - load_paths.flatten.compact.uniq.flat_map{ |path| Dir["#{path}/**/*.rb"] } - end - - def router - @router ||= Router.new(self) + 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! # AbstractController::Callbacks::ClassMethods.public_instance_methods. - select { |m| m.match(/(filter|action)/) }.each do |name| + select { |m| m.end_with?('_action') }.each do |name| define_method name do |*args, &block| - controllers_for_filters.each do |controller| - controller.public_send name, *args, &block + ActiveSupport.on_load(:active_admin_controller) do + public_send name, *args, &block end end end def controllers_for_filters controllers = [BaseController] - controllers.push *Devise.controllers_for_filters if Dependency.devise? + controllers.push(*Devise.controllers_for_filters) if Dependency.devise? controllers end - private - - def register_default_assets - register_stylesheet 'active_admin.css', media: 'screen' - register_stylesheet 'active_admin/print.css', media: 'print' - - register_javascript 'active_admin.js' - end + private # 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. @@ -262,7 +168,7 @@ def register_default_assets # files from being loaded twice in production. def remove_active_admin_load_paths_from_rails_autoload_and_eager_load ActiveSupport::Dependencies.autoload_paths -= load_paths - Rails.application.config.eager_load_paths -= load_paths + Rails.application.config.eager_load_paths -= load_paths end # Hook into the Rails code reloading mechanism so that things are reloaded @@ -279,11 +185,11 @@ def attach_reloader # 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. - ActionDispatch::Reloader.to_prepare(prepend: true, &unload_active_admin) + 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. - ActionDispatch::Reloader.to_cleanup(&unload_active_admin) + ActiveSupport::Reloader.to_complete(&unload_active_admin) end admin_dirs = {} @@ -298,7 +204,7 @@ def attach_reloader app.reloaders << routes_reloader - ActionDispatch::Reloader.to_prepare do + 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`. 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 55a292de894..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.clear - end - - def register_javascript(name) - javascripts.add name - end - - def javascripts - @javascripts ||= Set.new - end - - def clear_javascripts! - javascripts.clear - 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 d9e181acd0d..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)` # # @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. 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 a2140468961..91a362da38b 100644 --- a/lib/active_admin/batch_actions.rb +++ b/lib/active_admin/batch_actions.rb @@ -1,16 +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/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 edf9fa2d985..e3cb588ecc9 100644 --- a/lib/active_admin/batch_actions/controller.rb +++ b/lib/active_admin/batch_actions/controller.rb @@ -1,14 +1,12 @@ +# frozen_string_literal: true module ActiveAdmin module BatchActions module Controller - # Controller action that is called when submitting the batch action form def batch_action if action_present? - selection = params[:collection_selection] || [] - inputs = JSON.parse params[:batch_action_inputs] || '{}' - valid_keys = render_in_context(self, current_batch_action.inputs).try(:keys) - inputs = inputs.with_indifferent_access.slice *valid_keys + 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]}\"" @@ -22,7 +20,7 @@ def action_present? end def current_batch_action - active_admin_config.batch_actions.detect{ |action| action.sym.to_s == params[:batch_action] } + active_admin_config.batch_actions.detect { |action| action.sym.to_s == params[:batch_action] } end COLLECTION_APPLIES = [ @@ -30,7 +28,6 @@ def current_batch_action :filtering, :scoping, :includes, - :collection_decorator ].freeze def batch_action_collection(only = COLLECTION_APPLIES) diff --git a/lib/active_admin/batch_actions/resource_extension.rb b/lib/active_admin/batch_actions/resource_extension.rb index fa89f914e55..a4b9b8f6636 100644 --- a/lib/active_admin/batch_actions/resource_extension.rb +++ b/lib/active_admin/batch_actions/resource_extension.rb @@ -1,5 +1,5 @@ +# frozen_string_literal: true module ActiveAdmin - module BatchActions module ResourceExtension def initialize(*) @@ -51,34 +51,28 @@ def clear_batch_actions! @batch_actions = {} end - # Path to the batch action itself - def batch_action_path(params = {}) - path = [route_collection_path(params), "batch_action"].join("/") - query = params.slice(:q, :scope).to_param - [path, query].reject(&:blank?).join("?") - end - private # @return [ActiveAdmin::BatchAction] The default "delete" action 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) } + 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| + 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 @@ -89,9 +83,9 @@ class BatchAction include Comparable - attr_reader :block, :title, :sym + attr_reader :block, :title, :sym, :partial, :link_html_options - DEFAULT_CONFIRM_MESSAGE = proc{ I18n.t 'active_admin.batch_actions.default_confirmation' } + DEFAULT_CONFIRM_MESSAGE = proc { I18n.t "active_admin.batch_actions.default_confirmation" } # Create a Batch Action # @@ -101,7 +95,7 @@ class BatchAction # => 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) + # => 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") { |selection| } # => You can create batch actions with a title instead of a Symbol @@ -115,32 +109,32 @@ class BatchAction # BatchAction.new(:flag, confirm: "Are you sure?") { |selection| } # => You can pass a custom confirmation message through `:confirm` # - # BatchAction.new(:flag, form: {foo: :text, bar: :checkbox}) { |selection, inputs| } - # => You can pass a hash of options to `:form` that will be rendered as form input fields for the user to fill out. + # 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 - elsif !@confirm && @options[:form] - DEFAULT_CONFIRM_MESSAGE else @confirm end end - def inputs - @options[:form] - 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 - @options[:if] || proc{ true } + @options[:if] || proc { true } end # Used for sorting @@ -152,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 bf545f2ecc3..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,22 +1,18 @@ -require 'active_admin/component' +# frozen_string_literal: true +require_relative "../../component" module ActiveAdmin module BatchActions - # 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 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.batch_action_path(params), id: options[:id] - input name: :batch_action, id: :batch_action, type: :hidden + 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) @@ -30,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_selector.rb b/lib/active_admin/batch_actions/views/batch_action_selector.rb deleted file mode 100644 index cf370812141..00000000000 --- a/lib/active_admin/batch_actions/views/batch_action_selector.rb +++ /dev/null @@ -1,57 +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"), - class: "batch_actions_selector dropdown_menu", - 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, - "data-inputs" => render_in_context(self, batch_action.inputs).to_json - } - - 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 - - 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 f152181435f..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 @@ -27,10 +31,8 @@ class ResourceSelectionTogglePanel < ActiveAdmin::Component 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)") } + 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 a8e0cf02bb3..3ea5811cb37 100644 --- a/lib/active_admin/callbacks.rb +++ b/lib/active_admin/callbacks.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true module ActiveAdmin module Callbacks extend ActiveSupport::Concern + CALLBACK_TYPES = %i[before after].freeze + private # Simple callback system. Implements before and after callbacks for @@ -57,7 +60,7 @@ 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}" @@ -74,10 +77,10 @@ def define_active_admin_callbacks(*names) 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) } + 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) } + self.class.send(:"after_#{name}_callbacks").each { |cbk| run_callback(cbk, *args) } return value end send :private, "run_#{name}_callbacks" diff --git a/lib/active_admin/cancan_adapter.rb b/lib/active_admin/cancan_adapter.rb index 71dafb9fc3f..51ce4cc2bf0 100644 --- a/lib/active_admin/cancan_adapter.rb +++ b/lib/active_admin/cancan_adapter.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true unless ActiveAdmin::Dependency.cancan? || ActiveAdmin::Dependency.cancancan? ActiveAdmin::Dependency.cancan! end -require 'cancan' +require "cancan" # Add a setting to the application to configure the ability ActiveAdmin::Application.inheritable_setting :cancan_ability_class, "Ability" 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/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 a47aa4616a1..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 # @@ -22,7 +23,7 @@ class CSVBuilder def self.default_for_resource(resource) new resource: resource do column :id - resource.content_columns.each { |c| column c.name.to_sym } + resource.content_columns.each { |c| column c } end end @@ -30,61 +31,65 @@ def self.default_for_resource(resource) COLUMN_TRANSITIVE_OPTIONS = [:humanize_name].freeze - def initialize(options={}, &block) + def initialize(options = {}, &block) @resource = options.delete(:resource) - @columns, @options, @block = [], options, block + @columns = [] + @options = ActiveAdmin.application.csv_options.merge options + @block = block end - def column(name, options={}, &block) + def column(name, options = {}, &block) @columns << Column.new(name, @resource, column_transitive_options.merge(options), block) end - def build(controller, receiver) - @collection = controller.send(:find_collection, except: :pagination) - options = ActiveAdmin.application.csv_options.merge self.options + 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 - if byte_order_mark = options.delete(:byte_order_mark) - receiver << byte_order_mark - end + csv << bom if bom - if options.delete(:column_names) { true } - receiver << CSV.generate_line( - columns.map { |c| encode c.name, options }, - options.except(:encoding_options)) + if column_names + csv << CSV.generate_line(columns.map { |c| sanitize(encode(c.name, options)) }, **csv_options) end - (1..paginated_collection.total_pages).each do |page_no| - paginated_collection(page_no).each do |resource| - resource = controller.send :apply_decorator, resource - receiver << CSV.generate_line( - build_row(resource, columns, options), - options.except(:encoding_options)) - end + controller.send(:in_paginated_batches) do |resource| + csv << CSV.generate_line(build_row(resource, columns, options), **csv_options) end + + csv end 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? + instance_exec(&@block) if @block.present? columns end def build_row(resource, columns, options) columns.map do |column| - encode call_method_or_proc_on(resource, column.data), options + sanitize(encode(call_method_or_proc_on(resource, column.data), options)) end end def encode(content, options) if options[:encoding] - content.to_s.encode options[:encoding], options[:encoding_options] + 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 @@ -106,7 +111,7 @@ def initialize(name, resource = nil, options = {}, block = nil) def humanize_name(name, resource, humanize_name_option) if humanize_name_option - name.is_a?(Symbol) && resource.present? ? resource.human_attribute_name(name) : name.to_s.humanize + name.is_a?(Symbol) && resource ? resource.resource_class.human_attribute_name(name) : name.to_s.humanize else name.to_s end @@ -118,13 +123,22 @@ def humanize_name(name, resource, humanize_name_option) 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) - def paginated_collection(page_no = 1) - @collection.public_send(Kaminari.config.page_method_name, page_no).per(batch_size) + value end - def batch_size - 1000 + 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 index d0aba2f2bee..11179d02c49 100644 --- a/lib/active_admin/dependency.rb +++ b/lib/active_admin/dependency.rb @@ -1,31 +1,32 @@ +# frozen_string_literal: true module ActiveAdmin module Dependency module Requirements - DEVISE = '~> 3.2' + DEVISE = ">= 4.0", "< 5" end # Provides a clean interface to check for gem dependencies at runtime. # - # ActiveAdmin::Dependency.draper - # => # + # ActiveAdmin::Dependency.rails + # => # # - # ActiveAdmin::Dependency.draper? + # ActiveAdmin::Dependency.rails? # => true # - # ActiveAdmin::Dependency.draper? '>= 1.5.0' + # ActiveAdmin::Dependency.rails? '>= 6.1' # => false # - # ActiveAdmin::Dependency.draper? '= 1.2.1' + # ActiveAdmin::Dependency.rails? '= 6.0.3.2' # => true # - # ActiveAdmin::Dependency.draper? '~> 1.2.0' + # ActiveAdmin::Dependency.rails? '~> 6.0.3' # => true # - # ActiveAdmin::Dependency.rails? '>= 4.1.0', '<= 4.1.1' + # ActiveAdmin::Dependency.rails? '>= 6.0.3', '<= 6.1.0' # => true # - # ActiveAdmin::Dependency.rails! '2' - # -> ActiveAdmin::DependencyError: You provided rails 3.2.18 but we need: 2. + # 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. @@ -33,18 +34,18 @@ module Requirements # # All but the pessimistic operator (~>) can also be run using Ruby's comparison syntax. # - # ActiveAdmin::Dependency.rails >= '3.2.18' + # 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-ui-rails'] < 5 + # ActiveAdmin::Dependency['jquery-rails'] < 5 # => false # def self.method_missing(name, *args) - if name[-1] == '?' + if name[-1] == "?" Matcher.new(name[0..-2]).match? args - elsif name[-1] == '!' + elsif name[-1] == "!" Matcher.new(name[0..-2]).match! args else Matcher.new name.to_s @@ -87,9 +88,10 @@ def <=>(other) end def inspect - info = spec ? "#{spec.name} #{spec.version}" : '(missing)' + info = spec ? "#{spec.name} #{spec.version}" : "(missing)" "" end end + end end diff --git a/lib/active_admin/deprecation.rb b/lib/active_admin/deprecation.rb deleted file mode 100644 index 8ee1d6a9b77..00000000000 --- a/lib/active_admin/deprecation.rb +++ /dev/null @@ -1,35 +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.send :define_method, "deprecated_#{method}", klass.instance_method(method) - - klass.send :define_method, method do |*args| - ActiveAdmin::Deprecation.warn "#{message}", caller - send "deprecated_#{method}", *args - end - end - - end -end diff --git a/lib/active_admin/devise.rb b/lib/active_admin/devise.rb index 943d14c4ad7..8de2fd29603 100644 --- a/lib/active_admin/devise.rb +++ b/lib/active_admin/devise.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true ActiveAdmin::Dependency.devise! ActiveAdmin::Dependency::Requirements::DEVISE -require 'devise' +require "devise" module ActiveAdmin module Devise @@ -9,8 +10,7 @@ def self.config { path: ActiveAdmin.application.default_namespace || "/", controllers: ActiveAdmin::Devise.controllers, - path_names: { sign_in: 'login', sign_out: "logout" }, - sign_out_via: [*::Devise.sign_out_via, ActiveAdmin.application.logout_link_method].uniq + path_names: { sign_in: "login", sign_out: "logout" } } end @@ -27,14 +27,15 @@ def self.controllers 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("_") path = if Helpers::Routes.respond_to? root_path_method Helpers::Routes.send root_path_method @@ -43,31 +44,41 @@ def root_path "/#{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 + include ::ActiveAdmin::Devise::Controller + + ActiveSupport.run_load_hooks(:active_admin_controller, self) end class ConfirmationsController < ::Devise::ConfirmationsController - include ::ActiveAdmin::Devise::Controller + include ::ActiveAdmin::Devise::Controller + + ActiveSupport.run_load_hooks(:active_admin_controller, self) end def self.controllers_for_filters diff --git a/lib/active_admin/dsl.rb b/lib/active_admin/dsl.rb index a5438af28ec..cbe6a9de6c9 100644 --- a/lib/active_admin/dsl.rb +++ b/lib/active_admin/dsl.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin # The Active Admin DSL. This class is where all the registration blocks @@ -12,7 +13,7 @@ def initialize(config) # Runs the registration block inside this object def run_registration_block(&block) - instance_exec &block if block_given? + instance_exec(&block) if block end # The instance of ActiveAdmin::Resource that's being registered @@ -29,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: @@ -71,7 +72,7 @@ def include(mod) # end # def controller(&block) - @config.controller.class_exec(&block) if block_given? + @config.controller.class_exec(&block) if block @config.controller end @@ -83,14 +84,7 @@ def controller(&block) # this action item on. # :except: A single or array of controller actions not to # display this action item on. - def action_item(name = nil, options = {}, &block) - if name.is_a?(Hash) - options = name - name = nil - end - - Deprecation.warn "using `action_item` without a name is deprecated! Use `action_item(:edit)`." unless name - + def action_item(name, options = {}, &block) config.add_action_item(name, options, &block) end @@ -108,7 +102,7 @@ def action_item(name = nil, options = {}, &block) def batch_action(title, options = {}, &block) # Create symbol & title information if title.is_a? String - sym = title.titleize.tr(' ', '').underscore.to_sym + sym = title.titleize.tr(" ", "").underscore.to_sym else sym = title title = sym.to_s.titleize @@ -116,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 @@ -137,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 @@ -163,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 725a961ecff..0d1930bc2bc 100644 --- a/lib/active_admin/engine.rb +++ b/lib/active_admin/engine.rb @@ -1,16 +1,45 @@ +# frozen_string_literal: true module ActiveAdmin class Engine < ::Rails::Engine + 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| - ActiveAdmin.application.stylesheets.each do |path, _| - app.config.assets.precompile << path + 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 - ActiveAdmin.application.javascripts.each do |path| - app.config.assets.precompile << path + + 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 'active_admin/helpers/routes/url_helpers' + 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 index 957e7fd0121..824a7fe79f6 100644 --- a/lib/active_admin/error.rb +++ b/lib/active_admin/error.rb @@ -1,3 +1,4 @@ +# 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 @@ -5,8 +6,10 @@ module ActiveAdmin class AccessDenied < StandardError attr_reader :user, :action, :subject - def initialize(user, action, subject) - @user, @action, @subject = user, action, subject + def initialize(user, action, subject = nil) + @user = user + @action = action + @subject = subject super() end @@ -22,7 +25,7 @@ class Error < RuntimeError 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 + backtrace.grep(/\/(#{folder}\/.*\.rb):(\d+)/) { [$1, $2] }.first end end @@ -33,7 +36,7 @@ def initialize(exception) 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." + + "One solution can be, to wrap the query in a Proc. " + "Original error message: #{exception.message}" end @@ -43,8 +46,6 @@ def self.capture raise new exception end - private - def self.database_error_classes @classes ||= [] end diff --git a/lib/active_admin/event.rb b/lib/active_admin/event.rb deleted file mode 100644 index f549be6a303..00000000000 --- a/lib/active_admin/event.rb +++ /dev/null @@ -1,24 +0,0 @@ -module ActiveAdmin - - class EventDispatcher - def subscribe(*event_names, &block) - Deprecation.warn "`ActiveAdmin::Event.subscribe` is deprecated, use `ActiveSupport::Notifications.subscribe`" - event_names.each do |event| - ActiveSupport::Notifications.subscribe event, - &wrap_block_for_active_support_notifications(block) - end - end - - def dispatch(event, *args) - Deprecation.warn "`ActiveAdmin::Event.dispatch` is deprecated, use `ActiveSupport::Notifications.publish`" - ActiveSupport::Notifications.publish event, *args - end - - def wrap_block_for_active_support_notifications block - proc { |event, *args| block.call *args } - 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 index d01e7652962..2d9bc43ad89 100644 --- a/lib/active_admin/filters/active.rb +++ b/lib/active_admin/filters/active.rb @@ -1,29 +1,32 @@ -require 'active_admin/filters/humanized' +# frozen_string_literal: true +require_relative "active_filter" module ActiveAdmin module Filters - class Active - attr_accessor :filters, :scope + attr_reader :filters, :resource, :scopes - def initialize(resource_class, params) - @resource_class, @params = resource_class, params - @scope = humanize_scope - @filters = build_filters - end + # Instantiate a `Active` object containing collection of current active filters - private + # @param resource [ActiveAdmin::Resource] current resource + # @param search [Ransack::Search] search object + # + # @see ActiveAdmin::ResourceController::DataAccess#apply_filtering + def initialize(resource, search) + @resource = resource + @filters = build_filters(search.conditions) + @scopes = search.instance_variable_get(:@scope_args) + end - def build_filters - @params[:q] ||= [] - @params[:q].map { |param| Humanized.new(param) } + def all_blank? + filters.blank? && scopes.blank? end - def humanize_scope - scope = @params['scope'] - scope ? scope.humanize : "All" + 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 a7a9db1f857..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 diff --git a/lib/active_admin/filters/forms.rb b/lib/active_admin/filters/forms.rb index 530036c745f..7c71fdf74a4 100644 --- a/lib/active_admin/filters/forms.rb +++ b/lib/active_admin/filters/forms.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Filters # This form builder defines methods to build filter forms such @@ -6,9 +7,6 @@ class FormBuilder < ::ActiveAdmin::FormBuilder include ::ActiveAdmin::Filters::FormtasticAddons self.input_namespaces = [::Object, ::ActiveAdmin::Inputs::Filters, ::ActiveAdmin::Inputs, ::Formtastic::Inputs] - # TODO: remove input class finders after formtastic 4 (where it will be default) - self.input_class_finder = ::Formtastic::InputClassFinder - def filter(method, options = {}) if method.present? && options[:as] ||= default_input_type(method) template.concat input(method, options) @@ -20,7 +18,7 @@ def filter(method, options = {}) # 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 =~ /_(eq|equals|cont|contains|start|starts_with|end|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 @@ -30,7 +28,7 @@ def default_input_type(method, options = {}) case column.type when :date, :datetime :date_range - when :string, :text + when :string, :text, :citext :string when :integer, :float, :decimal :numeric @@ -40,44 +38,5 @@ def default_input_type(method, options = {}) end 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.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]) - - f.filter attribute, opts.except(:if, :unless) - 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: except_hidden_fields) - end - - f.template.concat buttons - end - end - - private - - def except_hidden_fields - [:q, :page] - end - end - end end diff --git a/lib/active_admin/filters/formtastic_addons.rb b/lib/active_admin/filters/formtastic_addons.rb index 9921a7c666c..9728ed6fb81 100644 --- a/lib/active_admin/filters/formtastic_addons.rb +++ b/lib/active_admin/filters/formtastic_addons.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Filters module FormtasticAddons @@ -37,7 +38,7 @@ def klass end def polymorphic_foreign_type?(method) - klass.reflect_on_all_associations.select{ |r| r.macro == :belongs_to && r.options[:polymorphic] } + klass.reflect_on_all_associations.select { |r| r.macro == :belongs_to && r.options[:polymorphic] } .map(&:foreign_type).include? method.to_s end @@ -46,7 +47,7 @@ def polymorphic_foreign_type?(method) # def searchable_has_many_through? - if reflection && reflection.options[:through] + if klass.ransackable_associations.include?(method.to_s) && reflection && reflection.options[:through] reflection.through_reflection.klass.ransackable_attributes.include? reflection.foreign_key else false @@ -54,7 +55,7 @@ def searchable_has_many_through? end def seems_searchable? - has_predicate? || ransacker? || scope? + column_for(method).nil? && (has_predicate? || scope?) end # If the given method has a predicate (like _eq or _lteq), it's pretty @@ -63,11 +64,6 @@ def has_predicate? !!Ransack::Predicate.detect_from_string(method.to_s) end - # Ransack lets you define custom search methods, called ransackers. - def ransacker? - klass._ransackers.key? method.to_s - end - # Ransack supports exposing selected scopes on your model for advanced searches. def scope? context = Ransack::Context.for klass diff --git a/lib/active_admin/filters/humanized.rb b/lib/active_admin/filters/humanized.rb deleted file mode 100644 index 3ff5e152273..00000000000 --- a/lib/active_admin/filters/humanized.rb +++ /dev/null @@ -1,68 +0,0 @@ -module ActiveAdmin - module Filters - - class Humanized - include ActiveAdmin::ViewHelpers - - def initialize(param) - @body = param[0] - @value = param[1] - end - - def value - @value.is_a?(::Array) ? @value.compact.join(', ') : @value - end - - def body - predicate = ransack_predicate_translation - - if current_predicate.nil? - predicate = @body.titleize - elsif translation_missing?(predicate) - predicate = active_admin_predicate_translation - end - - "#{parse_parameter_body} #{predicate}".strip - end - - private - - def parse_parameter_body - return if current_predicate.nil? - - # Accounting for strings that might contain other predicates. Example: - # 'requires_approval' contains the substring 'eq' - split_string = "_#{current_predicate}" - - @body.split(split_string) - .first - .gsub('_', ' ') - .strip - .titleize - .gsub('Id', 'ID') - end - - def current_predicate - @current_predicate ||= predicates.detect { |p| @body.include?(p) } - end - - def predicates - Ransack::Predicate.names_by_decreasing_length - end - - def ransack_predicate_translation - I18n.t("ransack.predicates.#{current_predicate}") - end - - def active_admin_predicate_translation - translation = I18n.t("active_admin.filters.predicates.#{current_predicate}").downcase - end - - def translation_missing?(predicate) - predicate.downcase.include?('translation missing') - end - - end - - end -end diff --git a/lib/active_admin/filters/resource_extension.rb b/lib/active_admin/filters/resource_extension.rb index 1b339519562..1e73d0af880 100644 --- a/lib/active_admin/filters/resource_extension.rb +++ b/lib/active_admin/filters/resource_extension.rb @@ -1,20 +1,19 @@ -require 'active_admin/filters/active' +# frozen_string_literal: true +require_relative "active" module ActiveAdmin module Filters - class Disabled < RuntimeError - def initialize - super "Can't remove a filter when filters are disabled. Enable filters with 'config.filters = true'" + 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_search_status_sidebar_section + add_active_search_sidebar_section end # Returns the filters for this resource. If filters are not enabled, @@ -62,7 +61,7 @@ def preserve_default_filters? # # @param [Symbol] attributes The attributes to not filter on def remove_filter(*attributes) - raise Disabled unless filters_enabled? + raise Disabled, "remove" unless filters_enabled? attributes.each { |attribute| (@filters_to_remove ||= []) << attribute.to_sym } end @@ -74,7 +73,7 @@ def remove_filter(*attributes) # @param [Hash] options The set of options that are passed through to # ransack for the field definition. def add_filter(attribute, options = {}) - raise Disabled unless filters_enabled? + raise Disabled, "add" unless filters_enabled? (@filters ||= {})[attribute.to_sym] = options end @@ -85,7 +84,7 @@ def reset_filters! @filters_to_remove = nil end - private + private # Collapses the waveform, if you will, of which filters should be displayed. # Removes filters and adds in default filters as desired. @@ -99,7 +98,7 @@ def filter_lookup end if @filters_to_remove - @filters_to_remove.each &filters.method(:delete) + @filters_to_remove.each { |filter| filters.delete(filter) } end filters @@ -109,7 +108,7 @@ def filter_lookup def default_filters result = [] result.concat default_association_filters if namespace.include_default_association_filters - result.concat default_content_filters + result.concat content_columns result.concat custom_ransack_filters result end @@ -125,25 +124,40 @@ def custom_ransack_filters # Returns a default set of filters for the associations def default_association_filters 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] } + 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 } + not_poly.reject! { |r| r.chain.length > 2 } filters = poly.map(&:foreign_type) + not_poly.map(&:name) - filters.map &:to_sym + + # 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 + + 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.map{ |c| 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 @@ -151,44 +165,24 @@ def add_filters_sidebar_section end def filters_sidebar_section - ActiveAdmin::SidebarSection.new :filters, only: :index, if: ->{ 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 - def add_search_status_sidebar_section - if current_filters_enabled? - self.sidebar_sections << search_status_section - end + def add_active_search_sidebar_section + self.sidebar_sections << active_search_sidebar_section end - def search_status_section - ActiveAdmin::SidebarSection.new :search_status, only: :index, if: -> { params[:q] || params[:scope] } do - active = ActiveAdmin::Filters::Active.new(resource_class, params) - - span do - h4 I18n.t("active_admin.search_status.headline"), style: 'display: inline' - b active.scope, style: "display: inline" - - div style: "margin-top: 10px" do - h4 I18n.t("active_admin.search_status.current_filters"), style: 'margin-bottom: 10px' - ul do - if active.filters.blank? - li I18n.t("active_admin.search_status.no_current_filters") - else - active.filters.each do |filter| - li do - span filter.body - b filter.value - end - end - end - end - end - 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 44a449390ec..5b0e0525fea 100644 --- a/lib/active_admin/form_builder.rb +++ b/lib/active_admin/form_builder.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Provides an intuitive way to build has_many associated records in the same form. module Formtastic module Inputs @@ -13,129 +14,170 @@ def input_wrapping(&block) module ActiveAdmin class FormBuilder < ::Formtastic::FormBuilder - self.input_namespaces = [::Object, ::ActiveAdmin::Inputs, ::Formtastic::Inputs] - # TODO: remove both class finders after formtastic 4 (where it will be default) - self.input_class_finder = ::Formtastic::InputClassFinder - self.action_class_finder = ::Formtastic::ActionClassFinder + self.input_namespaces = [::Object, ::ActiveAdmin::Inputs, ::Formtastic::Inputs] - 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 + 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 attr_accessor :already_in_an_inputs_block - def assoc_heading(assoc) - object.class.reflect_on_association(assoc).klass.model_name. - human(count: ::ActiveAdmin::Helpers::I18n::PLURAL_MANY_COUNT) + def has_many(assoc, options = {}, &block) + HasManyBuilder.new(self, assoc, options).render(&block) end + end - def has_many(assoc, options = {}, &block) - # remove options that should not render as attributes - custom_settings = :new_record, :allow_destroy, :heading, :sortable, :sortable_start - builder_options = {new_record: true}.merge! options.slice *custom_settings - options = {for: assoc }.merge! options.except *custom_settings - options[:class] = [options[:class], "inputs has_many_fields"].compact.join(' ') - sortable_column = builder_options[:sortable] - sortable_start = builder_options.fetch(:sortable_start, 0) + # 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(assoc, sortable_column)] + @options[:for] = [assoc, sorted_children(sortable_column)] end + end + def render(&block) html = "".html_safe - unless builder_options.key?(:heading) && !builder_options[:heading] - html << template.content_tag(:h3) do - builder_options[:heading] || assoc_heading(assoc) - end - end - - html << template.capture do - contents = "".html_safe - form_block = proc do |has_many_form| - index = parent_child_index options[:parent] if options[:parent] - block.call has_many_form, index - template.concat has_many_actions(has_many_form, builder_options, "".html_safe) - end - - template.assign(has_many_block: true) - contents = without_wrapper { inputs(options, &form_block) } || "".html_safe - - if builder_options[:new_record] - contents << js_for_has_many(assoc, form_block, template, builder_options[:new_record], options[:class]) - else - contents - end - end - - tag = @already_in_an_inputs_block ? :li : :div - html = template.content_tag(tag, html, class: "has_many_container #{assoc}", 'data-sortable' => sortable_column, 'data-sortable-start' => sortable_start) + 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 protected - def has_many_actions(has_many_form, builder_options, contents) - if has_many_form.object.new_record? - contents << template.content_tag(:li) do - template.link_to I18n.t('active_admin.has_many_remove'), "#", class: 'button has_many_remove' - end - elsif builder_options[:allow_destroy] - has_many_form.input(:_destroy, as: :boolean, - wrapper_html: {class: 'has_many_delete'}, - label: I18n.t('active_admin.has_many_delete')) + # 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 + + def default_heading + assoc_klass.model_name.human(count: 2.1) + end + + def assoc_klass + @assoc_klass ||= __getobj__.object.class.reflect_on_association(assoc).klass + end + + def content_has_many(&block) + form_block = proc do |form_builder| + render_has_many_form(form_builder, options[:parent], &block) end - if builder_options[:sortable] - has_many_form.input builder_options[:sortable], as: :hidden + template.assigns[:has_many_block] = true + contents = without_wrapper { inputs(options, &form_block) } + contents ||= "".html_safe + + js = new_record ? js_for_has_many(&form_block) : "" + contents << js + end + + # 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 - contents << template.content_tag(:li, class: 'handle') do - "MOVE" + 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 + 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 + + if sortable_column + form_builder.input sortable_column, as: :hidden + + # contents << template.content_tag(:li, class: "handle") do + # I18n.t("active_admin.move") + # end end contents end - def sorted_children(assoc, column) - object.public_send(assoc).sort_by do |o| + 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 + + 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 end - private - def without_wrapper - is_being_wrapped = @already_in_an_inputs_block - @already_in_an_inputs_block = false + is_being_wrapped = already_in_an_inputs_block + self.already_in_an_inputs_block = false html = yield - @already_in_an_inputs_block = is_being_wrapped + self.already_in_an_inputs_block = is_being_wrapped html end # Capture the ADD JS - def js_for_has_many(assoc, form_block, template, new_record, class_string) - assoc_reflection = object.class.reflect_on_association assoc - assoc_name = assoc_reflection.klass.model_name - placeholder = "NEW_#{assoc_name.to_s.underscore.upcase.gsub(/\//, '_')}_RECORD" - opts = { - for: [assoc, assoc_reflection.klass.new], - class: class_string, + 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{ 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) + ) + 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: "button has_many_add", data: { + template.link_to text, "#", class: "has-many-add", data: { html: CGI.escapeHTML(html).html_safe, placeholder: placeholder } 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/generators/boilerplate.rb b/lib/active_admin/generators/boilerplate.rb deleted file mode 100644 index 26de57c2f76..00000000000 --- a/lib/active_admin/generators/boilerplate.rb +++ /dev/null @@ -1,37 +0,0 @@ -module ActiveAdmin - module Generators - class Boilerplate - def initialize(class_name) - @class_name = class_name - end - - def attributes - @class_name.constantize.new.attributes.keys - end - - def rows - attributes.map { |a| row(a) }.join("\n") - end - - def row(name) - "# row :#{name.gsub(/_id$/, '')}" - end - - def columns - attributes.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 - end - end -end diff --git a/lib/active_admin/helpers/collection.rb b/lib/active_admin/helpers/collection.rb deleted file mode 100644 index ccacc7c1802..00000000000 --- a/lib/active_admin/helpers/collection.rb +++ /dev/null @@ -1,17 +0,0 @@ -module ActiveAdmin - module Helpers - module Collection - # 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) - c = c.except :select, :order - - c.group_values.present? ? c.count.count : c.count - end - - def collection_is_empty?(c = collection) - collection_size(c) == 0 - end - end - end -end diff --git a/lib/active_admin/helpers/i18n.rb b/lib/active_admin/helpers/i18n.rb deleted file mode 100644 index 2156fdbc5cd..00000000000 --- a/lib/active_admin/helpers/i18n.rb +++ /dev/null @@ -1,7 +0,0 @@ -module ActiveAdmin - module Helpers - module I18n - PLURAL_MANY_COUNT = 2.1 - end - end -end diff --git a/lib/active_admin/helpers/optional_display.rb b/lib/active_admin/helpers/optional_display.rb index 825a63d6689..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 @@ -15,14 +16,14 @@ module ActiveAdmin 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.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 index a94059ee60b..bacbe721d21 100644 --- a/lib/active_admin/helpers/routes/url_helpers.rb +++ b/lib/active_admin/helpers/routes/url_helpers.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Helpers module Routes diff --git a/lib/active_admin/helpers/scope_chain.rb b/lib/active_admin/helpers/scope_chain.rb index 7ed496d24ff..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: diff --git a/lib/active_admin/helpers/settings.rb b/lib/active_admin/helpers/settings.rb deleted file mode 100644 index 72c17fb67e3..00000000000 --- a/lib/active_admin/helpers/settings.rb +++ /dev/null @@ -1,115 +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 - - # Create an accessor that looks up the default value if none is set. - define_method name do - if instance_variable_defined? "@#{name}" - instance_variable_get "@#{name}" - else - read_default_setting name.to_sym - end - end - - define_method "#{name}?" do - value = public_send(name) - if value.is_a? Array - value.any? - else - value.present? - end - end - 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/inputs.rb b/lib/active_admin/inputs.rb index b634f4f2949..110eb0a437d 100644 --- a/lib/active_admin/inputs.rb +++ b/lib/active_admin/inputs.rb @@ -1,15 +1,14 @@ +# frozen_string_literal: true module ActiveAdmin module Inputs extend ActiveSupport::Autoload - autoload :DatepickerInput - module Filters extend ActiveSupport::Autoload autoload :Base autoload :StringInput - autoload :DatePickerInput + autoload :TextInput autoload :DateRangeInput autoload :NumericInput autoload :SelectInput diff --git a/lib/active_admin/inputs/datepicker_input.rb b/lib/active_admin/inputs/datepicker_input.rb deleted file mode 100644 index 7f8b71d6446..00000000000 --- a/lib/active_admin/inputs/datepicker_input.rb +++ /dev/null @@ -1,20 +0,0 @@ -module ActiveAdmin - module Inputs - class DatepickerInput < ::Formtastic::Inputs::StringInput - def input_html_options - super.tap do |options| - options[:class] = [options[:class], "datepicker"].compact.join(' ') - options[:data] ||= {} - options[:data].merge! datepicker_options - end - end - - private - def datepicker_options - options = self.options.fetch(:datepicker_options, {}) - options = Hash[options.map{ |k, v| [k.to_s.camelcase(:lower), v] }] - { datepicker_options: options } - end - end - end -end diff --git a/lib/active_admin/inputs/filters/base.rb b/lib/active_admin/inputs/filters/base.rb index 48a18710357..3eba474bcea 100644 --- a/lib/active_admin/inputs/filters/base.rb +++ b/lib/active_admin/inputs/filters/base.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Inputs module Filters @@ -24,7 +25,9 @@ def label_from_options end def wrapper_html_options - { class: "filter_form_field filter_#{as}" } + opts = super + (opts[:class] ||= "") << " filters-form-field" + opts end # Override the standard finder to accept a proc diff --git a/lib/active_admin/inputs/filters/base/search_method_select.rb b/lib/active_admin/inputs/filters/base/search_method_select.rb index 0c2f8adf114..06046e54c42 100644 --- a/lib/active_admin/inputs/filters/base/search_method_select.rb +++ b/lib/active_admin/inputs/filters/base/search_method_select.rb @@ -1,14 +1,15 @@ +# 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 FilterNumericInput < ::Formtastic::Inputs::NumberInput -# include FilterBase -# include FilterBase::SearchMethodSelect +# class NumericInput < ::Formtastic::Inputs::NumberInput +# include Base +# include Base::SearchMethodSelect # -# filter :equals, :greater_than, :less_than +# filter :eq, :gt, :lt # end # module ActiveAdmin @@ -25,21 +26,19 @@ module ClassMethods attr_reader :filters def filter(*filters) - (@filters ||= []).push *filters + (@filters ||= []).push(*filters) end end - def wrapper_html_options - opts = super - (opts[:class] ||= '') << ' select_and_search' unless seems_searchable? - 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 + [ + 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 @@ -48,7 +47,7 @@ def input_html end def select_html - template.select_tag '', template.options_for_select(filter_options, current_filter) + template.select_tag "", template.options_for_select(filter_options, current_filter), "data-search-methods": "" end def filters @@ -57,14 +56,14 @@ def filters def current_filter @current_filter ||= begin - methods = filters.map{ |f| "#{method}_#{f}" } - methods.detect{ |m| @object.public_send m } || methods.first + methods = filters.map { |f| "#{method}_#{f}" } + methods.detect { |m| @object.public_send m } || methods.first end end def filter_options filters.collect do |filter| - [I18n.t("active_admin.filters.predicates.#{filter}"), "#{method}_#{filter}"] + [Ransack::Translate.predicate(filter).capitalize, "#{method}_#{filter}"] end end diff --git a/lib/active_admin/inputs/filters/boolean_input.rb b/lib/active_admin/inputs/filters/boolean_input.rb index 1adc8575413..978addee236 100644 --- a/lib/active_admin/inputs/filters/boolean_input.rb +++ b/lib/active_admin/inputs/filters/boolean_input.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Inputs module Filters @@ -16,7 +17,7 @@ def input_html_options_name # Provide the AA translation to the blank input field. def include_blank - I18n.t 'active_admin.any' if super + I18n.t "active_admin.any" if super 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 index 63d9088696e..0aa31ea4743 100644 --- a/lib/active_admin/inputs/filters/check_boxes_input.rb +++ b/lib/active_admin/inputs/filters/check_boxes_input.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Inputs module Filters @@ -9,7 +10,7 @@ def input_name end def selected_values - @object.public_send("#{searchable_method_name}_in") || [] + @object.public_send(:"#{searchable_method_name}_in") || [] end def searchable_method_name @@ -20,11 +21,6 @@ def searchable_method_name end 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) diff --git a/lib/active_admin/inputs/filters/date_picker_input.rb b/lib/active_admin/inputs/filters/date_picker_input.rb deleted file mode 100644 index 0e26445cd89..00000000000 --- a/lib/active_admin/inputs/filters/date_picker_input.rb +++ /dev/null @@ -1,13 +0,0 @@ -module ActiveAdmin - module Inputs - module Filters - class DatePickerInput < ::Formtastic::Inputs::DatePickerInput - include Base - - def input_html_options - super.merge(class: "datepicker") - 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 index af176a49409..edb33f215cf 100644 --- a/lib/active_admin/inputs/filters/date_range_input.rb +++ b/lib/active_admin/inputs/filters/date_range_input.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Inputs module Filters @@ -7,9 +8,10 @@ class DateRangeInput < ::Formtastic::Inputs::StringInput 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)), + '
', + 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 @@ -23,12 +25,29 @@ def lt_input_name "#{method}_lteq" end - def input_html_options(input_name = gt_input_name) - current_value = @object.public_send input_name + def input_html_options { size: 12, class: "datepicker", - maxlength: 10, - value: current_value.respond_to?(:strftime) ? current_value.strftime("%Y-%m-%d") : "" } + 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 diff --git a/lib/active_admin/inputs/filters/numeric_input.rb b/lib/active_admin/inputs/filters/numeric_input.rb index 5a5f30e248c..62838cab9e3 100644 --- a/lib/active_admin/inputs/filters/numeric_input.rb +++ b/lib/active_admin/inputs/filters/numeric_input.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Inputs module Filters @@ -5,7 +6,7 @@ class NumericInput < ::Formtastic::Inputs::NumberInput include Base include Base::SearchMethodSelect - filter :equals, :greater_than, :less_than + filter :eq, :gt, :lt end end end diff --git a/lib/active_admin/inputs/filters/select_input.rb b/lib/active_admin/inputs/filters/select_input.rb index cdaccc2e436..23c9a041022 100644 --- a/lib/active_admin/inputs/filters/select_input.rb +++ b/lib/active_admin/inputs/filters/select_input.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Inputs module Filters @@ -7,22 +8,20 @@ class SelectInput < ::Formtastic::Inputs::SelectInput def input_name return method if seems_searchable? - searchable_method_name.concat multiple? ? '_in' : '_eq' + searchable_method_name + (multiple? ? "_in" : "_eq") end def searchable_method_name if searchable_has_many_through? "#{reflection.through_reflection.name}_#{reflection.foreign_key}" else - name = method.to_s - name.concat '_id' if reflection - name + 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 + I18n.t "active_admin.any" if super end def input_html_options_name @@ -42,10 +41,16 @@ def collection 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").uniq.pluck method + klass.reorder("#{method} asc").distinct.pluck method + end + + def reflection_searchable? + reflection && !reflection.polymorphic? end end diff --git a/lib/active_admin/inputs/filters/string_input.rb b/lib/active_admin/inputs/filters/string_input.rb index 0acc1ba0a80..b9aa8a14bac 100644 --- a/lib/active_admin/inputs/filters/string_input.rb +++ b/lib/active_admin/inputs/filters/string_input.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Inputs module Filters @@ -5,7 +6,7 @@ class StringInput < ::Formtastic::Inputs::StringInput include Base include Base::SearchMethodSelect - filter :contains, :equals, :starts_with, :ends_with + 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. 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 af9aad4f848..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 @@ -47,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? @@ -60,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 @@ -93,7 +95,7 @@ def _add(options) def normalize_id(id) case id when String, Symbol, ActiveModel::Name - id.to_s.downcase.tr ' ', '_' + id.to_s.downcase.tr " ", "_" when ActiveAdmin::Resource::Name id.param_key else diff --git a/lib/active_admin/menu_collection.rb b/lib/active_admin/menu_collection.rb index 7e1769df539..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 @@ -24,7 +25,7 @@ def clear! end def exists?(menu_name) - @menus.keys.include? menu_name + @menus.key?(menu_name) end def fetch(menu_name) @@ -86,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 1e2378b0776..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 ancestry 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 9bf7bc05abc..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 @@ -25,19 +26,41 @@ module ActiveAdmin # 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 - attr_reader :application, :resources, :name, :menus + RegisterEvent = "active_admin.namespace.register".freeze + + 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, resource_class, &block) if block_given? + parse_registration_block(config, &block) if block reset_menu! # Dispatch a registration event - ActiveSupport::Notifications.publish 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.public_send name - end - def fetch_menu(name) @menus.fetch(name) end @@ -121,60 +141,18 @@ def build_menu(name = DEFAULT_MENU) end end - # The default logout menu item - # - # @param [ActiveAdmin::MenuItem] menu The menu to add the logout link to - # @param [Fixnum] priority The numeric priority for the order in which it appears - # @param [Hash] html_options An options hash to pass along to link_to - # - def add_logout_button_to_menu(menu, priority = 20, html_options = {}) - if logout_link_path - html_options = html_options.reverse_merge(method: logout_link_method || :get) - menu.add id: 'logout', priority: priority, html_options: html_options, - label: ->{ I18n.t 'active_admin.logout' }, - url: ->{ render_or_call_method_or_proc_on self, active_admin_namespace.logout_link_path }, - if: :current_active_admin_user? - end - end - - # The default user session menu item - # - # @param [ActiveAdmin::MenuItem] menu The menu to add the logout link to - # @param [Fixnum] priority The numeric priority for the order in which it appears - # @param [Hash] html_options An options hash to pass along to link_to - # - def add_current_user_to_menu(menu, priority = 10, html_options = {}) - if current_user_method - menu.add id: 'current_user', priority: priority, html_options: html_options, - label: -> { display_name current_active_admin_user }, - url: -> { auto_url_for(current_active_admin_user) }, - if: :current_active_admin_user? - end - end - protected def build_menu_collection @menus = MenuCollection.new @menus.on_build do - build_default_utility_nav - 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| - add_current_user_to_menu menu - add_logout_button_to_menu menu - end - end - # 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) @@ -192,9 +170,9 @@ def register_page_controller(config) def unload_resources! resources.each do |resource| - parent = (module_name || 'Object').constantize - name = resource.controller_name.split('::').last - parent.send(:remove_const, name) if parent.const_defined? 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 @@ -212,14 +190,14 @@ def register_module end end - # TODO replace `eval` with `Class.new` + # 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 end - def parse_registration_block(config, resource_class, &block) - config.dsl = ResourceDSL.new(config, resource_class) + def parse_registration_block(config, &block) + config.dsl = ResourceDSL.new(config) config.dsl.run_registration_block(&block) end diff --git a/lib/active_admin/namespace_settings.rb b/lib/active_admin/namespace_settings.rb new file mode 100644 index 00000000000..83f2ea9e98c --- /dev/null +++ b/lib/active_admin/namespace_settings.rb @@ -0,0 +1,94 @@ +# 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 + + # 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 index 5f0e93a610c..75c803f4df4 100644 --- a/lib/active_admin/order_clause.rb +++ b/lib/active_admin/order_clause.rb @@ -1,13 +1,14 @@ +# frozen_string_literal: true module ActiveAdmin class OrderClause - attr_reader :field, :order + attr_reader :field, :order, :active_admin_config - def initialize(clause) - clause =~ /^([\w\_\.]+)(->'\w+')?_(desc|asc)$/ + 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 @@ -15,12 +16,37 @@ def valid? @field.present? && @order.present? end - def to_sql(active_admin_config) - table = active_admin_config.resource_column_names.include?(@column) ? active_admin_config.resource_table_name : nil - table_column = (@column =~ /\./) ? @column : + 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 - [table_column, @op, ' ', @order].compact.join + 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 index 8b68ba2ee74..7fa22c66cc7 100644 --- a/lib/active_admin/orm/active_record.rb +++ b/lib/active_admin/orm/active_record.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true # ActiveRecord-specific plugins should be required here ActiveAdmin::DatabaseHitDuringLoad.database_error_classes << ActiveRecord::StatementInvalid -require 'active_admin/orm/active_record/comments' +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 index 6378a9d2484..510011cf891 100644 --- a/lib/active_admin/orm/active_record/comments.rb +++ b/lib/active_admin/orm/active_record/comments.rb @@ -1,21 +1,31 @@ -require 'active_admin/orm/active_record/comments/views' -require 'active_admin/orm/active_record/comments/show_page_helper' -require 'active_admin/orm/active_record/comments/namespace_helper' -require 'active_admin/orm/active_record/comments/resource_helper' +# 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 :show_comments_in_menu, true -ActiveAdmin::Application.inheritable_setting :comments_registration_name, 'Comment' -ActiveAdmin::Application.inheritable_setting :comments_order, "created_at ASC" +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 -ActiveAdmin.application.view_factory.show_page.send :include, ActiveAdmin::Comments::ShowPageHelper +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' +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| @@ -23,9 +33,9 @@ namespace.register ActiveAdmin::Comment, as: namespace.comments_registration_name do actions :index, :show, :create, :destroy - menu false unless namespace.comments && namespace.show_comments_in_menu + menu namespace.comments ? namespace.comments_menu : false - config.comments = false # Don't allow comments on comments + 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 @@ -40,49 +50,56 @@ # Store the author and namespace before_save do |comment| comment.namespace = active_admin_config.namespace.name - comment.author = current_active_admin_user + comment.author = current_active_admin_user end controller do # Prevent N+1 queries def scoped_collection - super.includes *( # rails/rails#14734 - ActiveAdmin::Dependency.rails?('>= 4.1.0', '<= 4.1.1') ? - [:author] : [:author, :resource] - ) + super.includes(:author, :resource) end # Redirect to the resource show page after comment creation def create create! do |success, failure| - success.html{ redirect_to :back } + 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_to :back + 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 { redirect_to :back } - failure.html { redirect_to :back } + def destroy + destroy! do |success, failure| + success.html do + # If deleting from the Comments resource page then this will fail, as redirecting back + # will be to the comment show page, but comment was deleted. The following can be used + # to alleviate that, but then deleting comments on commentable resource pages will + # redirect to the comments index which may be undesirable. + # redirect_to({ action: :index }, fallback_location: active_admin_root) + redirect_back fallback_location: active_admin_root + end + failure.html do + redirect_back fallback_location: active_admin_root end end end end - # Set up permitted params in case the app is using Strong Parameters - unless Rails::VERSION::MAJOR == 3 && !defined? StrongParameters - permit_params :body, :namespace, :resource_id, :resource_type - 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.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 - column I18n.t('active_admin.comments.created_at'), :created_at + 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 diff --git a/lib/active_admin/orm/active_record/comments/comment.rb b/lib/active_admin/orm/active_record/comments/comment.rb index 68c10651349..e1b2643eb2a 100644 --- a/lib/active_admin/orm/active_record/comments/comment.rb +++ b/lib/active_admin/orm/active_record/comments/comment.rb @@ -1,14 +1,11 @@ +# frozen_string_literal: true module ActiveAdmin class Comment < ActiveRecord::Base - self.table_name = 'active_admin_comments' + self.table_name = "#{table_name_prefix}active_admin_comments#{table_name_suffix}" - belongs_to :resource, polymorphic: true - belongs_to :author, polymorphic: true - - unless Rails::VERSION::MAJOR > 3 && !defined? ProtectedAttributes - attr_accessible :resource, :resource_id, :resource_type, :body, :namespace - end + belongs_to :resource, polymorphic: true, optional: true + belongs_to :author, polymorphic: true validates_presence_of :body, :namespace, :resource @@ -19,27 +16,25 @@ def self.resource_type(resource) ResourceController::Decorators.undecorate(resource).class.base_class.name.to_s end - # Postgres adapters won't compare strings to numbers (issue 34) - def self.resource_id_cast(record) - resource_id_type == :string ? record.id.to_s : record.id - 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 + resource_id: resource.id, + namespace: namespace.to_s ).order(ActiveAdmin.application.namespaces[namespace.to_sym].comments_order) end - def self.resource_id_type - columns.detect{ |i| i.name == "resource_id" }.type - 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/orm/active_record/comments/namespace_helper.rb b/lib/active_admin/orm/active_record/comments/namespace_helper.rb index 3c64559bdd0..14157f49c80 100644 --- a/lib/active_admin/orm/active_record/comments/namespace_helper.rb +++ b/lib/active_admin/orm/active_record/comments/namespace_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Comments diff --git a/lib/active_admin/orm/active_record/comments/resource_helper.rb b/lib/active_admin/orm/active_record/comments/resource_helper.rb index e67b295939d..b7e6f746731 100644 --- a/lib/active_admin/orm/active_record/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 diff --git a/lib/active_admin/orm/active_record/comments/show_page_helper.rb b/lib/active_admin/orm/active_record/comments/show_page_helper.rb deleted file mode 100644 index 3f8529569bf..00000000000 --- a/lib/active_admin/orm/active_record/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/orm/active_record/comments/views.rb b/lib/active_admin/orm/active_record/comments/views.rb index c882d06c212..f844704792b 100644 --- a/lib/active_admin/orm/active_record/comments/views.rb +++ b/lib/active_admin/orm/active_record/comments/views.rb @@ -1,2 +1,3 @@ -require 'active_admin/views' -require 'active_admin/orm/active_record/comments/views/active_admin_comments' +# 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 index bca28b5eb08..fdaa962f55a 100644 --- 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 @@ -1,88 +1,29 @@ -require 'active_admin/views' -require 'active_admin/views/components/panel' +# frozen_string_literal: true +require_relative "../../../../views" module ActiveAdmin module Comments module Views - - class Comments < ActiveAdmin::Views::Panel + class Comments < Arbre::Element builder_method :active_admin_comments_for - attr_accessor :resource - 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) : I18n.t('active_admin.comments.author_missing') - end - span pretty_format comment.created_at - if authorized?(ActiveAdmin::Auth::DESTROY, comment) - text_node link_to I18n.t('active_admin.comments.delete'), comments_url(comment.id), method: :delete, data: { confirm: I18n.t('active_admin.comments.delete_confirmation') } - end - end - div class: 'active_admin_comment_body' do - simple_format comment.body - end + 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 - def build_empty_message - span I18n.t('active_admin.comments.no_comments_yet'), class: 'empty' - end - - def comments_url(*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 + 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 - - def build_comment_form - 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(parent.resource) } - f.input :resource_id, as: :hidden, input_html: { value: parent.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' + parts << "path" + send parts.join "_" end end - end end end diff --git a/lib/active_admin/orm/mongoid.rb b/lib/active_admin/orm/mongoid.rb deleted file mode 100644 index 26570b3c0d0..00000000000 --- a/lib/active_admin/orm/mongoid.rb +++ /dev/null @@ -1 +0,0 @@ -# Mongoid-specific plugins should be required here diff --git a/lib/active_admin/page.rb b/lib/active_admin/page.rb index 461e2168a90..3912dc4e15a 100644 --- a/lib/active_admin/page.rb +++ b/lib/active_admin/page.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin # Page is the primary data storage for page configuration in Active Admin # @@ -65,7 +66,7 @@ def default_menu_options 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` @@ -73,10 +74,6 @@ def route_uncountable? false end - def belongs_to? - false - end - def add_default_action_items end @@ -88,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 83ef9eed850..94607aa81b4 100644 --- a/lib/active_admin/page_dsl.rb +++ b/lib/active_admin/page_dsl.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin # This is the class where all the register_page blocks are evaluated. class PageDSL < DSL @@ -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 91e2f580bdd..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 @@ -21,7 +22,8 @@ class PagePresenter 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 index 8556b0f9c65..c81abe11ce4 100644 --- a/lib/active_admin/pundit_adapter.rb +++ b/lib/active_admin/pundit_adapter.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true ActiveAdmin::Dependency.pundit! -require '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 @@ -17,11 +19,11 @@ def authorized?(action, subject = nil) end def scope_collection(collection, action = Auth::READ) - # scoping is appliable only to read/index action + # scoping is applicable only to read/index action # which means there is no way how to scope other actions - Pundit.policy_scope!(user, collection) + Pundit.policy_scope!(user, namespace(collection)) rescue Pundit::NotDefinedError => e - if default_policy_class && default_policy_class.const_defined?(:Scope) + if default_policy_class&.const_defined?(:Scope) default_policy_class::Scope.new(user, collection).resolve else raise e @@ -29,25 +31,20 @@ def scope_collection(collection, action = Auth::READ) end def retrieve_policy(subject) - case subject - when nil then Pundit.policy!(user, resource) - when Class then Pundit.policy!(user, subject.new) - else Pundit.policy!(user, subject) - end - rescue Pundit::NotDefinedError => e - if default_policy_class - default_policy(user, subject) + target = policy_target(subject) + if (policy = policy(namespace(target)) || compat_policy(subject)) + policy + elsif default_policy_class + default_policy(subject) else - raise e + raise Pundit::NotDefinedError, "unable to find a compatible policy for `#{target}`" end end def format_action(action, subject) - # https://github.com/elabs/pundit/blob/master/lib/generators/pundit/install/templates/application_policy.rb + # https://github.com/varvet/pundit/blob/main/lib/generators/pundit/install/templates/application_policy.rb case action - when Auth::CREATE then :create? - when Auth::UPDATE then :update? - when Auth::READ then subject.is_a?(Class) ? :index? : :show? + when Auth::READ then subject.is_a?(Class) ? :index? : :show? when Auth::DESTROY then subject.is_a?(Class) ? :destroy_all? : :destroy? else "#{action}?" end @@ -55,14 +52,72 @@ def format_action(action, subject) 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 && ActiveAdmin.application.pundit_default_policy.constantize + ActiveAdmin.application.pundit_default_policy&.constantize end - def default_policy(user, subject) + 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/resource.rb b/lib/active_admin/resource.rb index 65f3a149df8..b2c6c51788b 100644 --- a/lib/active_admin/resource.rb +++ b/lib/active_admin/resource.rb @@ -1,15 +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/includes' -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 @@ -24,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 @@ -41,7 +46,7 @@ class Resource # The default sort order to use in the controller attr_writer :sort_order def sort_order - @sort_order ||= (resource_class.respond_to?(:primary_key) ? resource_class.primary_key.to_s : 'id') + '_desc' + @sort_order ||= (resource_class.respond_to?(:primary_key) ? resource_class.primary_key.to_s : "id") + "_desc" end # Set the configuration for the CSV @@ -50,6 +55,12 @@ def sort_order # Set breadcrumb builder 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 @@ -60,10 +71,11 @@ def sort_order module Base def initialize(namespace, resource_class, options = {}) @namespace = namespace - @resource_class_name = "::#{resource_class.name}" - @options = options + @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 = [], [] + @member_actions = [] + @collection_actions = [] end end @@ -82,15 +94,21 @@ def initialize(namespace, resource_class, options = {}) include ScopeTo include Sidebars 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 @@ -121,7 +139,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 @@ -129,6 +148,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 @@ -143,9 +168,33 @@ 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 ? decorator_class.new(resource) : resource + 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 private @@ -153,16 +202,21 @@ def find_resource(id) def method_for_find(id) if finder = resources_configuration[:self][:finder] [finder, id] - elsif Rails::VERSION::MAJOR >= 4 - [:find_by, { resource_class.primary_key => id }] else - [:"find_by_#{resource_class.primary_key}", id] + [: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 1aae736ecee..0dc825392e0 100644 --- a/lib/active_admin/resource/action_items.rb +++ b/lib/active_admin/resource/action_items.rb @@ -1,4 +1,5 @@ -require 'active_admin/helpers/optional_display' +# frozen_string_literal: true +require_relative "../helpers/optional_display" module ActiveAdmin @@ -24,6 +25,7 @@ def action_items # this action item on. # :except: A single or array of controller actions not to # display this action item on. + # :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 @@ -38,7 +40,7 @@ def remove_action_item(name) # # @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 @@ -57,34 +59,36 @@ def action_items? def add_default_action_items add_default_new_action_item add_default_edit_action_item - add_default_show_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 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_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 # Adds the default Edit link on show def add_default_edit_action_item - add_action_item :edit, 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 + 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 # Adds the default Destroy link on show - def add_default_show_action_item - add_action_item :destroy, 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 + 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 @@ -103,6 +107,10 @@ def initialize(name, options = {}, &block) @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..07e20b7167c --- /dev/null +++ b/lib/active_admin/resource/attributes.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true +module ActiveAdmin + + class Resource + module Attributes + + def default_attributes + 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 4ec21d55612..b65503f6ba9 100644 --- a/lib/active_admin/resource/belongs_to.rb +++ b/lib/active_admin/resource/belongs_to.rb @@ -1,4 +1,4 @@ -require 'active_admin/resource' +# frozen_string_literal: true module ActiveAdmin class Resource @@ -14,8 +14,13 @@ def initialize(key, namespace) # 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 @@ -39,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 512f34d25e4..c63ddd09115 100644 --- a/lib/active_admin/resource/controllers.rb +++ b/lib/active_admin/resource/controllers.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin class Resource module Controllers @@ -6,7 +7,7 @@ module Controllers # 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 index 15f2b73d1c9..0858b491b6c 100644 --- a/lib/active_admin/resource/includes.rb +++ b/lib/active_admin/resource/includes.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin class Resource module Includes diff --git a/lib/active_admin/resource/menu.rb b/lib/active_admin/resource/menu.rb index 771519f988c..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 @@ -25,13 +27,15 @@ def default_menu_options 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) } + 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 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 fc111a8e3d2..5c0c8813350 100644 --- a/lib/active_admin/resource/naming.rb +++ b/lib/active_admin/resource/naming.rb @@ -1,10 +1,11 @@ +# frozen_string_literal: true module ActiveAdmin class Resource module Naming def resource_name @resource_name ||= begin - as = @options[:as].gsub /\s/, '' if @options[:as] + as = @options[:as].gsub(/\s/, "") if @options[:as] if as || !resource_class.respond_to?(:model_name) Name.new resource_class, as @@ -17,13 +18,12 @@ def resource_name # Returns the name to call this resource such as "Bank Account" def resource_label resource_name.translate count: 1, - default: resource_name.to_s.gsub('::', ' ').titleize + default: resource_name.to_s.gsub("::", " ").titleize end # Returns the plural version of this resource such as "Bank Accounts" def plural_resource_label(options = {}) - defaults = {count: Helpers::I18n::PLURAL_MANY_COUNT, - default: resource_label.pluralize.titleize} + defaults = { count: 2.1, default: resource_label.pluralize.titleize } resource_name.translate defaults.merge options end @@ -46,7 +46,7 @@ def initialize(klass, name = nil) end def translate(options = {}) - I18n.t i18n_key, {scope: [:activerecord, :models]}.merge(options) + I18n.t i18n_key, **{ scope: [:activerecord, :models] }.merge(options) end def route_key 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 93d396602ca..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 @@ -33,7 +34,7 @@ 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 # @return [PagePresenter, nil] - def get_page_presenter(action, type=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 d66b8212d98..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 diff --git a/lib/active_admin/resource/routes.rb b/lib/active_admin/resource/routes.rb index b5ec172796f..7caea753a64 100644 --- a/lib/active_admin/resource/routes.rb +++ b/lib/active_admin/resource/routes.rb @@ -1,27 +1,40 @@ +# frozen_string_literal: true module ActiveAdmin class Resource module Routes # @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) - RouteBuilder.new(self).edit_instance_path(resource) + 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? @@ -30,39 +43,50 @@ def route_uncountable? 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) + routes.public_send route_name, *route_collection_params(params), additional_params + end + + def batch_action_path(params, additional_params = {}) + route_name = route_name( + resource.resources_configuration[:self][:route_collection_name], + action: :batch_action, + suffix: (resource.route_uncountable? ? "index_path" : "path") + ) + + 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) + 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) + routes.public_send route_name, *route_instance_params(instance), additional_params end - # @return [String] the path to the edit page of this resource + # @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 edit_instance_path(instance) + def member_action_path(action, instance, additional_params = {}) path = resource.resources_configuration[:self][:route_instance_name] - route_name = route_name(path, action: :edit) + route_name = route_name(path, action: action) - routes.public_send route_name, *route_instance_params(instance) + routes.public_send route_name, *route_instance_params(instance), additional_params end private @@ -73,20 +97,19 @@ def route_name(resource_path_name, options = {}) suffix = options[:suffix] || "path" route = [] - route << options[:action] # "edit" or "new" - 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.public_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 @@ -99,11 +122,19 @@ 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 diff --git a/lib/active_admin/resource/scope_to.rb b/lib/active_admin/resource/scope_to.rb index 592c1092df4..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 @@ -38,10 +39,10 @@ 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, + method: nil, association_method: nil, - if: nil, - unless: 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 f110c2ae5cc..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,7 @@ 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 diff --git a/lib/active_admin/resource_collection.rb b/lib/active_admin/resource_collection.rb index e704a7b16e2..2b7dc334a30 100644 --- a/lib/active_admin/resource_collection.rb +++ b/lib/active_admin/resource_collection.rb @@ -1,3 +1,4 @@ +# 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`. @@ -21,7 +22,7 @@ def add(resource) # Changes `each` to pass in the value, instead of both the key and value. def each(&block) - values.each &block + values.each(&block) end def [](obj) @@ -38,12 +39,12 @@ def find_resource(obj) 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 } + 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 + select { |r| r.class <= Resource } # can otherwise be a Page end def raise_if_mismatched!(existing, given) diff --git a/lib/active_admin/resource_controller.rb b/lib/active_admin/resource_controller.rb deleted file mode 100644 index e2bc9c5fd53..00000000000 --- a/lib/active_admin/resource_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -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/streaming' -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 ActionBuilder - include Decorators - include DataAccess - include Scoping - include Streaming - 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/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_controller/streaming.rb b/lib/active_admin/resource_controller/streaming.rb deleted file mode 100644 index d680a7fb7e0..00000000000 --- a/lib/active_admin/resource_controller/streaming.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'csv' - -module ActiveAdmin - class ResourceController < BaseController - - # This module overrides CSV responses to allow large data downloads. - # Could be expanded to JSON and XML in the future. - # - module Streaming - - def index - super do |format| - format.csv { stream_csv } - yield(format) if block_given? - end - end - - protected - - def stream_resource(&block) - headers['X-Accel-Buffering'] = 'no' - headers['Cache-Control'] = 'no-cache' - self.response_body = Enumerator.new &block - end - - def csv_filename - "#{resource_collection_name.to_s.gsub('_', '-')}-#{Time.zone.now.to_date.to_s(:default)}.csv" - end - - def stream_csv - headers['Content-Disposition'] = %{attachment; filename="#{csv_filename}"} - stream_resource &active_admin_config.csv_builder.method(:build).to_proc.curry[self] - end - - end - end -end diff --git a/lib/active_admin/resource_dsl.rb b/lib/active_admin/resource_dsl.rb index 28647d4553b..a4da5c0f349 100644 --- a/lib/active_admin/resource_dsl.rb +++ b/lib/active_admin/resource_dsl.rb @@ -1,13 +1,29 @@ +# frozen_string_literal: true module ActiveAdmin # This is the class where all the register blocks are evaluated. class ResourceDSL < DSL - def initialize(config, resource_class) - @resource = resource_class - super(config) - end 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 @@ -24,11 +40,11 @@ def scope(*args, &block) # Store relations that should be included def includes(*args) - config.includes.push *args + config.includes.push(*args) end # - # Rails 4 Strong Parameters Support + # Keys included in the `permitted_params` setting are automatically whitelisted. # # Either # @@ -45,16 +61,23 @@ def includes(*args) # end # end # - # Keys included in the `permitted_params` setting are automatically whitelisted. - # def permit_params(*args, &block) param_key = config.param_key.to_sym controller do define_method :permitted_params do - params.permit *active_admin_namespace.permitted_params, - param_key => block ? instance_exec(&block) : args + 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 @@ -86,8 +109,8 @@ def form(options = {}, &block) # column :name # end # - def csv(options={}, &block) - options[:resource] = @resource + def csv(options = {}, &block) + options[:resource] = config config.csv_builder = CSVBuilder.new(options, &block) end @@ -100,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 @@ -112,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 @@ -129,6 +154,13 @@ def collection_action(name, options = {}, &block) action config.collection_actions, 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 + # Defined Callbacks # # == After Build @@ -153,21 +185,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_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, to: :controller - delegate :after_filter, :skip_after_filter, to: :controller - delegate :around_filter, :skip_filter, to: :controller - if Rails::VERSION::MAJOR == 4 - delegate :before_action, :skip_before_action, to: :controller - delegate :after_action, :skip_after_action, to: :controller - delegate :around_action, :skip_action, to: :controller - end + 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 # diff --git a/lib/active_admin/router.rb b/lib/active_admin/router.rb index f000b2fd99e..f504c4ef2e7 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 do |namespaces| - namespaces.each do |namespace| - if namespace.root? - root namespace.root_to_options.merge(to: namespace.root_to) - else - namespace namespace.name do - root namespace.root_to_options.merge(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.flat_map{ |n| n.resources.values } - 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 if config.batch_actions_enabled? - 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}" => "#{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/scope.rb b/lib/active_admin/scope.rb index 9251c370813..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,6 +14,12 @@ class Scope # Scope.new('Published', :public) # # => Scope with name 'Published' and scope method :public # + # 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 @@ -24,34 +31,45 @@ class 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.parameterize("_") + @id = method.to_s.parameterize(separator: "_") else @scope_method ||= name.to_sym - @id = name.to_s.parameterize("_") + @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.fetch(:show_count, true) - @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 67fde3d91e0..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,7 +7,9 @@ class SidebarSection attr_accessor :name, :options, :block def initialize(name, options = {}, &block) - @name, @options, @block = name.to_s, options, block + @name = name.to_s + @options = options + @block = block normalize_display_options! end @@ -15,11 +18,6 @@ def id "#{name.downcase.underscore}_sidebar_section".parameterize end - # The title gets displayed within the section in the view - def title - I18n.t("active_admin.sidebars.#{name}", default: name.titlecase) - end - # If a block is not passed in, the name of the partial to render def partial_name options[:partial] || "#{name.downcase.tr(' ', '_')}_sidebar" diff --git a/lib/active_admin/version.rb b/lib/active_admin/version.rb index 2758f11cdf9..96e041392dd 100644 --- a/lib/active_admin/version.rb +++ b/lib/active_admin/version.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin - VERSION = '1.0.0.pre2' + VERSION = "4.0.0.beta21" end diff --git a/lib/active_admin/view_factory.rb b/lib/active_admin/view_factory.rb deleted file mode 100644 index 056d972362e..00000000000 --- a/lib/active_admin/view_factory.rb +++ /dev/null @@ -1,27 +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, - unsupported_browser: ActiveAdmin::Views::UnsupportedBrowser, - 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 - - # Register All The Pages - register 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 8ffc484d79d..d4c84d9ba50 100644 --- a/lib/active_admin/view_helpers.rb +++ b/lib/active_admin/view_helpers.rb @@ -1,19 +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 ActiveAdminApplicationHelper - include AutoLinkHelper - include BreadcrumbHelper - include DisplayHelper 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/auto_link_helper.rb b/lib/active_admin/view_helpers/auto_link_helper.rb deleted file mode 100644 index 69935a6f6fd..00000000000 --- a/lib/active_admin/view_helpers/auto_link_helper.rb +++ /dev/null @@ -1,45 +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") - # - def auto_link(resource, content = display_name(resource)) - if url = auto_url_for(resource) - link_to content, url - else - content - end - end - - # Like `auto_link`, except that it only returns a URL instead of a full tag - def auto_url_for(resource) - config = active_admin_resource_for(resource.class) - return unless config - - if config.controller.action_methods.include?("show") && - authorized?(ActiveAdmin::Auth::READ, resource) - url_for config.route_instance_path resource - elsif config.controller.action_methods.include?("edit") && - authorized?(ActiveAdmin::Auth::UPDATE, resource) - url_for config.route_edit_instance_path resource - end - end - - # Returns the ActiveAdmin::Resource instance for a class - def active_admin_resource_for(klass) - if respond_to? :active_admin_namespace - active_admin_namespace.resource_for klass - end - 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 3d3e01fd5a4..00000000000 --- a/lib/active_admin/view_helpers/breadcrumb_helper.rb +++ /dev/null @@ -1,33 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module BreadcrumbHelper - - # Returns an array of links to use in a breadcrumb - def breadcrumb_links(path = request.path) - # remove leading "/" and split up the URL - # and remove last since it's used as the page title - parts = path.split('/').select(&:present?)[0..-2] - - 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] - parent = active_admin_config.belongs_to_config.try :target - config = parent && parent.resource_name.route_key == parts[index-1] ? parent : active_admin_config - name = display_name config.find_resource part - end - name ||= I18n.t "activerecord.models.#{part.singularize}", count: ::ActiveAdmin::Helpers::I18n::PLURAL_MANY_COUNT, default: part.titlecase - - # Don't create a link if the resource's show action is disabled - if !config || config.defined_actions.include?(:show) - link_to name, '/' + parts[0..index].join('/') - else - name - end - 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 35bde812a90..00000000000 --- a/lib/active_admin/view_helpers/display_helper.rb +++ /dev/null @@ -1,61 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module DisplayHelper - - DISPLAY_NAME_FALLBACK = ->{ - name, klass = "", self.class - name << klass.model_name.human if klass.respond_to? :model_name - name << " ##{send(klass.primary_key)}" if klass.respond_to? :primary_key - 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) - render_in_context resource, display_name_method_for(resource) unless resource.nil? - end - - # 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 - - # 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 - localize object, format: active_admin_application.localize_format - else - if defined?(::ActiveRecord) && object.is_a?(ActiveRecord::Base) || - defined?(::Mongoid) && object.class.include?(Mongoid::Document) - auto_link object - else - display_name object - end - 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 df6d0b7c182..00000000000 --- a/lib/active_admin/view_helpers/download_format_links_helper.rb +++ /dev/null @@ -1,46 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module DownloadFormatLinksHelper - - def build_download_format_links(formats = self.class.formats) - params = request.query_parameters.except :format, :commit - div class: "download_links" do - span I18n.t('active_admin.download') - formats.each do |format| - a format.upcase, href: url_for(params: params, format: format) - end - end - end - - def self.included base - base.extend ClassMethods - end - - 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 to the list of available formats available for data - # export. You must register the extension prior to adding it here. - # @param [Symbol] format the mime type to add - # @return [Array] A copy of the updated formats array. - def add_format(format) - unless Mime::Type.lookup_by_extension format - raise ArgumentError, "Please register the #{format} mime type with `Mime::Type.register`" - end - @formats << format unless formats.include? format - formats - end - 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 a27128d0d70..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.flat_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.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 f5de2acaa97..00000000000 --- a/lib/active_admin/view_helpers/flash_helper.rb +++ /dev/null @@ -1,14 +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. - # Note Rails >= 4.1 normalizes keys to strings automatically. - def flash_messages - @flash_messages ||= flash.to_hash.with_indifferent_access.except(*active_admin_application.flash_keys_to_except) - 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 41df102821a..00000000000 --- a/lib/active_admin/view_helpers/form_helper.rb +++ /dev/null @@ -1,20 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module FormHelper - - 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, 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/method_or_proc_helper.rb b/lib/active_admin/view_helpers/method_or_proc_helper.rb index ba23a880d27..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 @@ -60,6 +66,8 @@ def call_method_or_proc_on(receiver, *args) else symbol_or_proc.call(receiver, *args) end + else + symbol_or_proc end end @@ -71,7 +79,7 @@ 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 @@ -83,7 +91,7 @@ def render_in_context(context, obj, *args) 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.public_send obj, *args else 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 d39cb0add53..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_exec(&action_item.block) - 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 index babb3446d4e..27c91e951ff 100644 --- a/lib/active_admin/views/components/active_admin_form.rb +++ b/lib/active_admin/views/components/active_admin_form.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Views class FormtasticProxy < ::Arbre::Rails::Forms::FormBuilderProxy @@ -16,10 +17,10 @@ def closing_tag end def to_s - opening_tag << children.to_s << closing_tag + opening_tag + children.to_s + closing_tag end end - + class ActiveAdminForm < FormtasticProxy builder_method :active_admin_form_for @@ -27,25 +28,25 @@ def build(resource, options = {}, &block) @resource = resource options = options.deep_dup options[:builder] ||= ActiveAdmin::FormBuilder - form_string = semantic_form_for(resource, options) do |f| + 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_given? + instance_eval(&block) if block - # Rails 4 sets multipart automatically if a file field is present, + # 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 !~ /multipart/ + if multipart? && !@opening_tag.include?('multipart') @opening_tag.sub!(/
#{legend}" : "" - klasses = ["inputs"] - klasses << options[:class] if options[:class] - @opening_tag = "
#{legend_tag}
    " + html_options = args.extract_options! + html_options[:class] ||= "inputs" + legend = args.shift if args.first.is_a?(::String) + legend = html_options.delete(:name) if html_options.key?(:name) + legend_tag = legend ? helpers.tag.legend(legend, class: "fieldset-title") : "" + fieldset_attrs = helpers.tag.attributes html_options + @opening_tag = "
    #{legend_tag}
      " @closing_tag = "
    " - super(*(args << options), &block) + super(*(args << html_options), &block) end end @@ -120,4 +153,4 @@ def build(form_builder, *args, &block) end end end -end \ No newline at end of file +end diff --git a/lib/active_admin/views/components/attributes_table.rb b/lib/active_admin/views/components/attributes_table.rb index 5100b875e81..78af5704ff1 100644 --- a/lib/active_admin/views/components/attributes_table.rb +++ b/lib/active_admin/views/components/attributes_table.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Views @@ -5,30 +6,26 @@ class AttributesTable < ActiveAdmin::Component builder_method :attributes_table_for def build(obj, *attrs) - @collection = is_array?(obj) ? obj : [obj] + @collection = Array.wrap(obj) @resource_class = @collection.first.class - options = { } + 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! - classes = [:row] - if options[:class] - classes << options[:class] - elsif title.present? - classes << "row-#{title.to_s.parameterize('_')}" - end - options[:class] = classes.join(' ') + options["data-row"] = title.to_s.parameterize(separator: "_") if title.present? @table << tr(options) do th do @@ -36,7 +33,7 @@ def row(*args, &block) end @collection.each do |record| td do - content_for(record, block || title) + content_for(record, block || data) end end end @@ -45,7 +42,7 @@ def row(*args, &block) protected def default_id_for_prefix - 'attributes_table' + "attributes_table" end # Build Colgroups @@ -58,54 +55,38 @@ def build_colgroups within @table do col # column for row headers @collection.each do |record| - classes = Arbre::HTML::ClassList.new - classes << cycle(:even, :odd, name: self.class.to_s) - classes << dom_class_name_for(record) - col(id: dom_id_for(record), class: classes) + col(id: dom_id_for(record)) end end end def header_content_for(attr) - if @resource_class.respond_to?(:human_attribute_name) - @resource_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(record, attr) - value = pretty_format find_attr_value(record, attr) + value = helpers.format_attribute record, attr value.blank? && current_arbre_element.children.to_s.empty? ? empty_value : value - end - - def find_attr_value(record, attr) - if attr.is_a?(Proc) - attr.call(record) - elsif attr =~ /\A(.+)_id\z/ && reflection_for(record.class, $1.to_sym) - record.public_send $1 - elsif record.respond_to? attr - record.public_send attr - elsif record.respond_to? :[] - record[attr] - end - end - - def reflection_for(klass, method) - klass.reflect_on_association method if klass.respond_to? :reflect_on_association + # 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 single_record? @single_record ||= @collection.size == 1 end - - def is_array?(obj) - obj.respond_to?(:each) && obj.respond_to?(:first) && !obj.is_a?(Hash) - end 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 cf91fe972fc..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 e1cf5cf0392..00000000000 --- a/lib/active_admin/views/components/columns.rb +++ /dev/null @@ -1,161 +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 create 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 Min 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 - - columns.each_with_index do |column, i| - is_last_column = i == (columns_count - 1) - column.set_column_styles(column_width, margin_size, is_last_column) - end - end - - def columns_span_count - count = 0 - columns.each do |column| - count += column.span_size - end - - count - end - - def columns - children.select { |child| child.is_a?(Column) } - end - end - - class Column < ActiveAdmin::Component - - attr_accessor :span_size, :max_width, :min_width - - # @param [Hash] options An options hash for the column - # - # @option 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: #{safe_width(max_width)};" - end - - if min_width - styles << "min-width: #{safe_width(min_width)};" - end - - styles << "margin-right: #{margin_width}%;" unless is_last_column - - set_attribute :style, styles.join(" ") - end - - private - - # Converts values without a '%' or 'px' suffix to a pixel value - def safe_width(width) - width.to_s.gsub(/\A(\d+)\z/, '\1px') - 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 e87c9ac2775..00000000000 --- a/lib/active_admin/views/components/dropdown_menu.rb +++ /dev/null @@ -1,71 +0,0 @@ -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' 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 97013b437a0..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 - # 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 - 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 - - diff --git a/lib/active_admin/views/components/paginated_collection.rb b/lib/active_admin/views/components/paginated_collection.rb index bede97dbc81..3169d537628 100644 --- a/lib/active_admin/views/components/paginated_collection.rb +++ b/lib/active_admin/views/components/paginated_collection.rb @@ -1,8 +1,6 @@ -require 'active_admin/helpers/collection' - +# frozen_string_literal: true module ActiveAdmin module Views - # Wraps the content with pagination and available formats. # # *Example:* @@ -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. # @@ -37,18 +35,18 @@ class PaginatedCollection < ActiveAdmin::Component # download_links => Download links override (false or [:csv, :pdf]) # def build(collection, options = {}) - @collection = collection - @params = options.delete(:params) - @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) + @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 @@ -65,30 +63,28 @@ def add_child(*args, &block) protected def build_pagination_with_formats(options) - div id: "index_footer" do - build_per_page_select if @per_page.is_a?(Array) + 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") - - download_links = @download_links.is_a?(Proc) ? instance_exec(&@download_links) : @download_links - - 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 class: "pagination_per_page" do - text_node "Per page:" - select do + 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 + selected: @collection.limit_value == per_page ? "selected" : nil ) end end @@ -96,68 +92,68 @@ def build_per_page_select end def build_pagination - options = {} - options[:params] = @params if @params + options = { views_prefix: :active_admin, outer_window: 1, window: 2 } + options[:params] = @params if @params options[:param_name] = @param_name if @param_name - if !@display_total && collection.respond_to?(:offset) + 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 = collection.offset(collection.current_page * @per_page.to_i).limit(1).count - options[:total_pages] = collection.current_page + offset + 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 - text_node paginate collection, options + text_node paginate @collection, **options end - include ::ActiveAdmin::Helpers::Collection - include ::ActiveAdmin::ViewHelpers::DownloadFormatLinksHelper - # 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 @display_total - 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 @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 + 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 + to: offset + collection_size(@collection) end else # Do not display total count, in order to prevent a `SELECT count(*)`. - # To do so we must not call `collection.num_pages` - offset = (collection.current_page - 1) * collection.limit_value + # 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 + 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 ccba21d43d9..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 @@ -7,8 +8,8 @@ class Panel < ActiveAdmin::Component def build(title, attributes = {}) super(attributes) add_class "panel" - @title = h3(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) @@ -21,18 +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 - - def header_action(*args) - action = args[0] - - @title << div(class: 'header_action') do - action - end - end end end end diff --git a/lib/active_admin/views/components/scopes.rb b/lib/active_admin/views/components/scopes.rb index e5700a5b06c..c7f89ced7b5 100644 --- a/lib/active_admin/views/components/scopes.rb +++ b/lib/active_admin/views/components/scopes.rb @@ -1,51 +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 - params = request.query_parameters.except :page, :scope, :commit, :format - - a href: url_for(scope: scope.id, params: params), 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 @@ -59,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 ae573eec361..00000000000 --- a/lib/active_admin/views/components/sidebar_section.rb +++ /dev/null @@ -1,34 +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) - add_class @section.custom_class if @section.custom_class - self.id = @section.id - build_sidebar_content - end - - # Renders attributes_table_for current resource - def attributes_table(*args, &block) - attributes_table_for resource, *args, &block - end - - protected - - def build_sidebar_content - if @section.block - rvalue = instance_exec(&@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 3621314d550..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 01b6682afd3..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,16 +6,11 @@ class StatusTag < ActiveAdmin::Component builder_method :status_tag def tag_name - 'span' + "span" end - def default_class_name - 'status_tag' - end - - # @overload status_tag(status, type = nil, options = {}) - # @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. + # @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 @@ -22,35 +18,40 @@ def default_class_name # @return [ActiveAdmin::Views::StatusTag] # # @example - # status_tag('In Progress') - # # => In Progress + # status_tag(true) + # # => Yes # # @example - # status_tag('active', :ok) - # # => Active + # status_tag(false) + # # => No # # @example - # status_tag('active', :ok, class: 'important', id: 'status_123', label: 'on') - # # => on + # status_tag(nil) + # # => Unknown # - def build(*args) - options = args.extract_options! - status = args[0] - type = args[1] + # @example + # status_tag('In Progress') + # # => In Progress + # + # @example + # status_tag('Active', class: 'important', id: 'status_123', label: 'on') + # # => on + # + def build(status, options = {}) label = options.delete(:label) classes = options.delete(:class) - status = convert_to_boolean_status(status) + boolean_status = convert_to_boolean_status(status) + status = boolean_status || 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 + 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 @@ -58,21 +59,19 @@ def build(*args) def convert_to_boolean_status(status) case status - when true, 'true' - 'Yes' - when false, 'false', nil - 'No' - else - status + when true, "true" + "Yes" + when false, "false" + "No" + when nil + "Unset" end end - def status_to_class(status) + def convert_status(status) case status when String, Symbol - status.to_s.titleize.gsub(/\s/, '').underscore - else - '' + status.to_s.titleize.delete(" ").underscore end end end diff --git a/lib/active_admin/views/components/table_for.rb b/lib/active_admin/views/components/table_for.rb index 23ff9d323df..c67961ffcd5 100644 --- a/lib/active_admin/views/components/table_for.rb +++ b/lib/active_admin/views/components/table_for.rb @@ -1,34 +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(obj, *attrs) - options = attrs.extract_options! - @sortable = options.delete(:sortable) - @collection = obj.respond_to?(:each) && !obj.is_a?(Hash) ? obj : [obj] + options = attrs.extract_options! + @sortable = options.delete(:sortable) + @collection = obj.respond_to?(:each) && !obj.is_a?(Hash) ? obj : [obj] @resource_class = options.delete(:i18n) @resource_class ||= @collection.klass if @collection.respond_to? :klass - @columns = [] - @row_class = options.delete(:row_class) + + @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) } + 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 @@ -39,9 +45,9 @@ 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 @@ -64,62 +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 + 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: params, order: "#{sort_key}_#{order_for_sort_key(sort_key)}" + th(attributes) do + link_to params: params, order: "#{sort_key}_#{order_for_sort_key(sort_key)}" do + svg = '' + + (col.pretty_title + svg).html_safe + 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 do |elem| - classes = [cycle('odd', 'even')] - - if @row_class - classes << @row_class.call(elem) - end - - tr(class: classes.flatten.join(' '), id: dom_id_for(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 - render_data col.data, item - end - end - - def render_data(data, item) - value = if data.is_a? Proc - data.call item - elsif item.respond_to? data - item.public_send data - elsif item.respond_to? :[] - item[data] - end - value = pretty_format(value) if data.is_a?(Symbol) - value = status_tag value if is_boolean? data, item - value - end - - def is_boolean?(data, item) - if item.respond_to? :has_attribute? - item.has_attribute?(data) && - item.column_for_attribute(data) && - item.column_for_attribute(data).type == :boolean + 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 @@ -128,7 +117,7 @@ def is_boolean?(data, item) # current_sort[1] #=> asc | desc def current_sort @current_sort ||= begin - order_clause = OrderClause.new params[:order] + order_clause = active_admin_config.order_clause.new(active_admin_config, params[:order]) if order_clause.valid? [order_clause.field, order_clause.order] @@ -144,8 +133,8 @@ 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 @@ -156,19 +145,13 @@ def default_options class Column - attr_accessor :title, :data , :html_class + attr_accessor :title, :title_id, :data, :html_class - def initialize(*args, &block) + def initialize(*args, &block) @options = args.extract_options! - @title = args[0] - html_classes = [:col] - if @options.has_key?(:class) - html_classes << @options.delete(:class) - elsif @title.present? - html_classes << "col-#{@title.to_s.parameterize('_')}" - end - @html_class = html_classes.join(' ') + @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] @@ -195,18 +178,9 @@ def sortable? # 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' - # def sort_key # If boolean or nil, use the default sort key. - if @options[:sortable] == true || @options[:sortable] == false - @data.to_s - elsif @options[:sortable].nil? + if @options[:sortable].nil? || @options[:sortable] == true || @options[:sortable] == false sort_column_name else @options[:sortable].to_s diff --git a/lib/active_admin/views/components/tabs.rb b/lib/active_admin/views/components/tabs.rb deleted file mode 100644 index ede61a3bff0..00000000000 --- a/lib/active_admin/views/components/tabs.rb +++ /dev/null @@ -1,28 +0,0 @@ -module ActiveAdmin - module Views - class Tabs < ActiveAdmin::Component - builder_method :tabs - - def tab(title, options = {}, &block) - title = title.to_s.titleize if title.is_a? Symbol - @menu << build_menu_item(title, options, &block) - @tabs_content << build_content_item(title, options, &block) - end - - def build(&block) - @menu = ul(class: 'nav nav-tabs', role: "tablist") - @tabs_content = div(class: 'tab-content') - end - - def build_menu_item(title, options, &block) - options = options.reverse_merge({}) - li { link_to title, "##{title.parameterize}", options } - end - - def build_content_item(title, options, &block) - options = options.reverse_merge(id: title.parameterize) - div(options, &block) - end - end - end -end diff --git a/lib/active_admin/views/components/unsupported_browser.rb b/lib/active_admin/views/components/unsupported_browser.rb deleted file mode 100644 index 3ee660fc317..00000000000 --- a/lib/active_admin/views/components/unsupported_browser.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveAdmin - module Views - class UnsupportedBrowser < Component - def build - h1 I18n.t("active_admin.unsupported_browser.headline").html_safe - para I18n.t("active_admin.unsupported_browser.recommendation").html_safe - para I18n.t("active_admin.unsupported_browser.turn_off_compatibility_view").html_safe - 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 cf168516e36..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 73786003b3a..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 6622c32dea9..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. - # - # ```ruby - # index as: :block do |product| - # div for: product do - # resource_selection_cell product - # h2 auto_link product.title - # div simple_format product.description - # 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 6a8b9d36fe4..00000000000 --- a/lib/active_admin/views/index_as_blog.rb +++ /dev/null @@ -1,156 +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. - # - # ```ruby - # 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: - # - # ```ruby - # index as: :blog do - # title :a_method_to_call - # end - # ``` - # - # 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: - # - # ```ruby - # 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: - # - # ```ruby - # index as: :blog do - # title :my_title - # body :my_body - # end - # ``` - # - # Or, render a block as the body: - # - # ```ruby - # 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_exec &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.public_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 2a90e1dbd7e..00000000000 --- a/lib/active_admin/views/index_as_grid.rb +++ /dev/null @@ -1,80 +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. - # - # ```ruby - # index as: :grid do |product| - # link_to image_tag(product.image_path), admin_product_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 columns that are rendered using the columns - # option: - # - # ```ruby - # index as: :grid, columns: 5 do |product| - # link_to image_tag(product.image_path), admin_product_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 6734b14b416..3b6a7ec1760 100644 --- a/lib/active_admin/views/index_as_table.rb +++ b/lib/active_admin/views/index_as_table.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Views @@ -71,7 +72,7 @@ module Views # selectable_column # column :title # actions do |post| - # item "Preview", admin_preview_post_path(post), class: "member_link" + # item "Preview", admin_preview_post_path(post), class: "preview-link" # end # end # ``` @@ -98,39 +99,13 @@ module Views # end # ``` # - # In case you prefer to list actions links in a dropdown menu: - # - # ```ruby - # index do - # selectable_column - # column :title - # actions dropdown: true do |post| - # item "Preview", admin_preview_post_path(post) - # end - # end - # ``` - # - # In addition, you can insert the position of the row in the greater collection by using the index_column special command: - # - # ```ruby - # index do - # selectable_column - # index_column - # column :title - # end - # ``` - # - # index_column take an optional offset parameter to allow a developer to set the starting number for the index (default is 1). - # # ## 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. # @@ -159,6 +134,24 @@ module Views # 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 + # ``` + # # ## Associated Sorting # # You're normally able to sort columns alphabetically, but by default you @@ -174,6 +167,13 @@ module Views # 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 @@ -196,47 +196,45 @@ module Views # end # ``` # - # ## Custom row class + # ## Custom tbody HTML attributes # - # In order to add special class to table rows pass the proc object as a `:row_class` option - # of the `index` method. + # In order to add HTML attributes to the tbody use the `:tbody_html` option. # # ```ruby - # index row_class: ->elem { 'active' if elem.active? } do + # 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, + 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 - selectable_column - id_column if resource_class.primary_key # View based Models have no primary_key - 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 - actions + else + render "index_as_table_default", table_options: table_options end end @@ -249,37 +247,33 @@ 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: 'col-selectable', sortable: false do |resource| + column resource_selection_toggle_cell, class: options[:class], sortable: false do |resource| resource_selection_cell resource end end - def index_column(start_value = 1) - column '#', class: 'col-index', sortable: false do |resource| - @collection.offset_value + @collection.index(resource) + start_value - end - end - # Display a column for the id - def id_column - raise "#{resource_class.name} as no primary_key!" unless resource_class.primary_key - column(resource_class.human_attribute_name(resource_class.primary_key), sortable: resource_class.primary_key) do |resource| - if controller.action_methods.include?('show') - 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 - def default_actions - raise '`default_actions` is no longer provided in ActiveAdmin 1.x. Use `actions` instead.' - end - # Add links to perform actions. # # ```ruby @@ -305,67 +299,41 @@ def default_actions # item 'Grant Admin', grant_admin_admin_user_path(admin_user) # end # - # # Append some actions onto the end of the default actions displayed in a Dropdown Menu - # actions dropdown: true do |admin_user| - # item 'Grant Admin', grant_admin_admin_user_path(admin_user) - # end - # - # # Custom actions without the defaults displayed in a Dropdown Menu. - # actions defaults: false, dropdown: true, dropdown_name: 'Additional actions' do |admin_user| - # item 'Grant Admin', grant_admin_admin_user_path(admin_user) - # end - # # ``` def actions(options = {}, &block) - name = options.delete(:name) { '' } - defaults = options.delete(:defaults) { true } - dropdown = options.delete(:dropdown) { false } - dropdown_name = options.delete(:dropdown_name) { I18n.t 'active_admin.dropdown_actions.button_label', default: 'Actions' } - - options[:class] ||= 'col-actions' + name = options.delete(:name) { "" } + defaults = options.delete(:defaults) { true } column name, options do |resource| - if dropdown - dropdown_menu dropdown_name do - defaults(resource) if defaults - instance_exec(resource, &block) if block_given? - end - else - table_actions do - defaults(resource, css_class: :member_link) if defaults - if block_given? - block_result = instance_exec(resource, &block) - text_node block_result unless block_result.is_a? Arbre::Element - end + 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 end end - private + private - def defaults(resource, options = {}) - if controller.action_methods.include?('show') && authorized?(ActiveAdmin::Auth::READ, resource) - item I18n.t('active_admin.view'), resource_path(resource), class: "view_link #{options[:css_class]}" - end - if controller.action_methods.include?('edit') && authorized?(ActiveAdmin::Auth::UPDATE, resource) - item I18n.t('active_admin.edit'), edit_resource_path(resource), class: "edit_link #{options[:css_class]}" - end - if controller.action_methods.include?('destroy') && authorized?(ActiveAdmin::Auth::DESTROY, resource) - item I18n.t('active_admin.delete'), resource_path(resource), class: "delete_link #{options[:css_class]}", - method: :delete, data: {confirm: I18n.t('active_admin.delete_confirmation')} - end + 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 class TableActions < ActiveAdmin::Component - builder_method :table_actions - - def item *args - text_node link_to *args + def item *args, **kwargs + text_node link_to(*args, **kwargs) end end end # IndexTableFor - - end # IndexAsTable + 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 b40d678fd36..00000000000 --- a/lib/active_admin/views/pages/base.rb +++ /dev/null @@ -1,147 +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].tr('/', '_')) - @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_namespace.site_title)].compact.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 - - if active_admin_namespace.favicon - text_node(favicon_link_tag(active_admin_namespace.favicon)) - end - - active_admin_namespace.meta_tags.each do |name, content| - text_node(tag(:meta, name: name, content: content)) - end - - text_node csrf_meta_tag - end - end - - def build_page - within @body do - div id: "wrapper" do - build_unsupported_browser - build_header - build_title_bar - build_page_content - build_footer - end - end - end - - def build_unsupported_browser - if active_admin_namespace.unsupported_browser_matcher =~ env["HTTP_USER_AGENT"] - insert_tag view_factory.unsupported_browser - 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 - div class: 'flashes' do - flash_messages.each do |type, message| - div message, class: "flash flash_#{type}" - 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: title).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/form.rb b/lib/active_admin/views/pages/form.rb deleted file mode 100644 index 1073e6e9960..00000000000 --- a/lib/active_admin/views/pages/form.rb +++ /dev/null @@ -1,61 +0,0 @@ -module ActiveAdmin - module Views - module Pages - - class Form < Base - - def title - assigns[:page_title] || I18n.t("active_admin.#{normalized_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 - options = default_form_options.merge form_presenter.options - - if options[:partial] - render options[:partial] - else - active_admin_form_for resource, options, &form_presenter.block - end - end - - private - - def default_form_options - { - url: default_form_path, - as: active_admin_config.param_key - } - end - - def default_form_path - resource.persisted? ? resource_path(resource) : collection_path - end - - def default_form_config - ActiveAdmin::PagePresenter.new do |f| - f.semantic_errors # show errors on :base by default - f.inputs - f.actions - end - end - - def normalized_action - case params[:action] - when "create" - "new" - when "update" - "edit" - else - params[:action] - 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 3415de39638..00000000000 --- a/lib/active_admin/views/pages/index.rb +++ /dev/null @@ -1,163 +0,0 @@ -require 'active_admin/helpers/collection' - -module ActiveAdmin - module Views - module Pages - - class Index < Base - - def title - if Proc === config[:title] - controller.instance_exec &config[:title] - else - config[:title] || assigns[:page_title] || active_admin_config.plural_resource_label - end - end - - # Retrieves the given page presenter, or uses the default. - def config - active_admin_config.get_page_presenter(:index, params[:as]) || - ActiveAdmin::PagePresenter.new(as: :table) - end - - # Renders 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.fetch(:scope_count, true) - } - - 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 - - # 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') && authorized?(ActiveAdmin::Auth::CREATE, active_admin_config.resource_class) - blank_slate_content = [blank_slate_content, blank_slate_link].compact.join(" ") - 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.fetch(:paginator, true) - download_links = config.fetch(:download_links, active_admin_config.namespace.download_links) - pagination_total = config.fetch(:pagination_total, true) - per_page = config.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 - div class: 'index_content' do - insert_tag(renderer_class, config, collection) - end - end - end - - private - - def blank_slate_link - if config.options.has_key?(:blank_slate_link) - blank_slate_link = config.options[:blank_slate_link] - if blank_slate_link.is_a?(Proc) - instance_exec(&blank_slate_link) - end - else - default_blank_slate_link - end - end - - def default_blank_slate_link - link_to(I18n.t("active_admin.blank_slate.link"), new_resource_path) - 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 55d058f888c..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 407823bb2c1..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 - assigns[:page_title] || 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.blank? - title = "#{active_admin_config.resource_label} ##{resource.id}" - end - - title - end - - module DefaultMainContent - def default_main_content(&block) - attributes_table(*default_attribute_table_rows, &block) - 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 f72c838d623..00000000000 --- a/lib/active_admin/views/tabbed_navigation.rb +++ /dev/null @@ -1,66 +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] - - if url = item.url(self) - text_node link_to item.label(self), url, item.html_options - else - span item.label(self), item.html_options - end - - 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 416ecd9c758..00000000000 --- a/lib/active_admin/views/title_bar.rb +++ /dev/null @@ -1,55 +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 = "/") - breadcrumb_config = active_admin_config && active_admin_config.breadcrumb - - links = if breadcrumb_config.is_a?(Proc) - instance_exec(controller, &active_admin_config.breadcrumb) - elsif breadcrumb_config.present? - 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 bb699e2ef1e..6773124496f 100644 --- a/lib/generators/active_admin/assets/assets_generator.rb +++ b/lib/generators/active_admin/assets/assets_generator.rb @@ -1,14 +1,15 @@ +# frozen_string_literal: true module ActiveAdmin module Generators class AssetsGenerator < Rails::Generators::Base - - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) def install_assets - template 'active_admin.js.coffee', 'app/assets/javascripts/active_admin.js.coffee' - template "active_admin.scss", "app/assets/stylesheets/active_admin.scss" + 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 - end end end 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/active_admin.js.coffee b/lib/generators/active_admin/assets/templates/active_admin.js.coffee deleted file mode 100644 index 3752dcef6be..00000000000 --- a/lib/generators/active_admin/assets/templates/active_admin.js.coffee +++ /dev/null @@ -1 +0,0 @@ -#= require active_admin/base diff --git a/lib/generators/active_admin/assets/templates/active_admin.scss b/lib/generators/active_admin/assets/templates/active_admin.scss deleted file mode 100644 index 90ba1d475e0..00000000000 --- a/lib/generators/active_admin/assets/templates/active_admin.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/active_admin/orm/mongoid/.gitkeep b/lib/generators/active_admin/assets/templates/builds/.keep similarity index 100% rename from lib/active_admin/orm/mongoid/.gitkeep rename to lib/generators/active_admin/assets/templates/builds/.keep 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 bcc4f7cf79d..2e764a6d6e2 100644 --- a/lib/generators/active_admin/devise/devise_generator.rb +++ b/lib/generators/active_admin/devise/devise_generator.rb @@ -1,5 +1,6 @@ -require "active_admin/error" -require "active_admin/dependency" +# frozen_string_literal: true +require_relative "../../../active_admin/error" +require_relative "../../../active_admin/dependency" module ActiveAdmin module Generators @@ -7,13 +8,13 @@ class DeviseGenerator < Rails::Generators::NamedBase desc "Creates an admin user and uses Devise for authentication" argument :name, type: :string, default: "AdminUser" - class_option :registerable, type: :boolean, default: false, - desc: "Should the generated resource be registerable?" + class_option :registerable, type: :boolean, default: false, + desc: "Should the generated resource be registerable?" RESERVED_NAMES = [:active_admin_user] - class_option :default_user, :type => :boolean, :default => true, - :desc => "Should a default user be created inside the migration?" + class_option :default_user, type: :boolean, default: true, + desc: "Should a default user be created inside the migration?" def install_devise begin @@ -22,9 +23,12 @@ def install_devise raise ActiveAdmin::GeneratorError, "#{e.message} If you don't want to use devise, run the generator with --skip-users." end - require 'devise' + require "devise" - if File.exists?(File.join(destination_root, "config", "initializers", "devise.rb")) + 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" @@ -52,11 +56,11 @@ def set_namespace_for_path end def add_default_user_to_seed - seeds_paths = Rails.application.paths["db/seeds.rb"] || Rails.application.paths["db/seeds"] # "db/seeds" => Rails 3.2 fallback + 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')" + 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 diff --git a/lib/generators/active_admin/install/install_generator.rb b/lib/generators/active_admin/install/install_generator.rb index eb408a1ec4c..fa1150e3fc9 100644 --- a/lib/generators/active_admin/install/install_generator.rb +++ b/lib/generators/active_admin/install/install_generator.rb @@ -1,4 +1,5 @@ -require 'rails/generators/active_record' +# frozen_string_literal: true +require "rails/generators/active_record" module ActiveAdmin module Generators @@ -7,21 +8,23 @@ class InstallGenerator < ActiveRecord::Generators::Base argument :name, type: :string, default: "AdminUser" 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" - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) def copy_initializer - @underscored_user_name = name.underscore + @underscored_user_name = name.underscore.tr("/", "_") @use_authentication_method = options[:users].present? - template 'active_admin.rb.erb', 'config/initializers/active_admin.rb' + @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 @@ -38,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 caca3cff280..1432cfc4dbe 100644 --- a/lib/generators/active_admin/install/templates/active_admin.rb.erb +++ b/lib/generators/active_admin/install/templates/active_admin.rb.erb @@ -2,21 +2,28 @@ 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: Aim for an image that's 21px high so it fits 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 = "logo.png" + # eg: + # config.load_paths = [ + # File.join(Rails.root, 'app', 'admin'), + # File.join(Rails.root, 'app', 'cashier') + # ] # == Default Namespace # @@ -61,14 +68,21 @@ ActiveAdmin.setup do |config| # 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 or make your own. Please refer to documentation. + # 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" @@ -94,18 +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 @@ -119,17 +127,20 @@ ActiveAdmin.setup do |config| # This allows your users to comment on any resource registered with Active Admin. # # You can completely disable comments: - # config.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 + # 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 # @@ -142,32 +153,25 @@ ActiveAdmin.setup do |config| # 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 - # == Localize Date/Time Format + # == Attribute Filters # - # Set the localize format to display dates and times. - # To understand how to localize your app with I18n, read more at - # https://github.com/svenfuchs/i18n/blob/master/lib%2Fi18n%2Fbackend%2Fbase.rb#L52 + # You can exclude possibly sensitive model attributes from being displayed, + # added to forms, or exported by default by ActiveAdmin # - config.localize_format = :long + config.filter_attributes = [:encrypted_password, :password, :password_confirmation] - # == Setting a Favicon + # == Localize Date/Time Format # - # config.favicon = 'favicon.ico' - - # == Meta Tags + # 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 # - # Add additional meta tags to the head element of active admin pages. + # You can run `bin/rails runner 'puts I18n.t("date.formats")'` to see the + # available formats in your application. # - # Add tags to all pages logged in users see: - # config.meta_tags = { author: 'My Company' } - - # By default, sign up/sign in/recover password pages are excluded - # from showing up in search engine results by adding a robots meta - # tag. You can reset the hash of meta tags included in logged out - # pages: - # config.meta_tags_for_logged_out_pages = {} + config.localize_format = :long # == Removing Breadcrumbs # @@ -176,20 +180,12 @@ ActiveAdmin.setup do |config| # # config.breadcrumb = false - # == Register Stylesheets & Javascripts - # - # We recommend using the built in Active Admin layout and loading - # up your own stylesheets / javascripts to customize the look - # and feel. + # == Create Another Checkbox # - # To load a stylesheet: - # config.register_stylesheet 'my_stylesheet.css' + # Create another checkbox is disabled by default. You can customize it for individual + # resources or you can enable them globally from here. # - # 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 - # - # To load a javascript file: - # config.register_javascript 'my_javascript.js' + # config.create_another = true # == CSV options # @@ -203,20 +199,11 @@ ActiveAdmin.setup do |config| # # 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 @@ -232,11 +219,10 @@ 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) + # # Enable/disable the links based on block (for example, with cancan) # admin.download_links = proc { can?(:view_download_links) } # # end @@ -261,9 +247,29 @@ ActiveAdmin.setup do |config| # config.filters = true # # By default the filters include associations in a select, which means - # that every record will be loaded for each association. + # 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 85% 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 472f594e506..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,8 +1,6 @@ ActiveAdmin.register <%= @user_class %> do -<% unless Rails::VERSION::MAJOR < 4 -%> permit_params :email, :password, :password_confirmation -<% end -%> index do selectable_column id_column @@ -19,7 +17,7 @@ ActiveAdmin.register <%= @user_class %> do filter :created_at form do |f| - f.inputs "Admin Details" do + f.inputs do f.input :email f.input :password f.input :password_confirmation diff --git a/lib/generators/active_admin/install/templates/dashboard.rb b/lib/generators/active_admin/install/templates/dashboard.rb index eae7baf0d70..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.erb similarity index 52% rename from lib/generators/active_admin/install/templates/migrations/create_active_admin_comments.rb rename to lib/generators/active_admin/install/templates/migrations/create_active_admin_comments.rb.erb index 9efd14781c2..0ce57948203 100644 --- 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.erb @@ -1,16 +1,13 @@ -class CreateActiveAdminComments < ActiveRecord::Migration +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.string :resource_id, null: false - t.string :resource_type, null: false + t.references :resource, polymorphic: true 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 diff --git a/lib/generators/active_admin/page/page_generator.rb b/lib/generators/active_admin/page/page_generator.rb index 03b45238b5e..0330610cc17 100644 --- a/lib/generators/active_admin/page/page_generator.rb +++ b/lib/generators/active_admin/page/page_generator.rb @@ -1,12 +1,12 @@ +# frozen_string_literal: true module ActiveAdmin module Generators class PageGenerator < Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) + 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 index 2d3eac7a9a8..638db219ed4 100644 --- a/lib/generators/active_admin/page/templates/page.rb +++ b/lib/generators/active_admin/page/templates/page.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true ActiveAdmin.register_page "<%= class_name %>" do content do # your content diff --git a/lib/generators/active_admin/resource/resource_generator.rb b/lib/generators/active_admin/resource/resource_generator.rb index 8998874e507..87d1e2b4392 100644 --- a/lib/generators/active_admin/resource/resource_generator.rb +++ b/lib/generators/active_admin/resource/resource_generator.rb @@ -1,20 +1,64 @@ -require 'active_admin/generators/boilerplate' - +# frozen_string_literal: true module ActiveAdmin module Generators class ResourceGenerator < Rails::Generators::NamedBase desc "Registers resources with Active Admin" - class_option :include_boilerplate, type: :boolean, default: false, - desc: "Generate boilerplate code for your resource." - - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) def generate_config_file - @boilerplate = ActiveAdmin::Generators::Boilerplate.new(class_name) - template "admin.rb", "app/admin/#{file_path.tr('/', '_')}.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 9f300b7693c..00000000000 --- a/lib/generators/active_admin/resource/templates/admin.rb +++ /dev/null @@ -1,39 +0,0 @@ -ActiveAdmin.register <%= class_name %> do -<% if Rails::VERSION::MAJOR == 4 || defined?(ActionController::StrongParameters) %> -# See permitted parameters documentation: -# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters -# -# permit_params :list, :of, :attributes, :on, :model -# -# or -# -# permit_params do -# permitted = [:permitted, :attributes] -# permitted << :other if resource.something? -# permitted -# end -<% end %> -<% if options.include_boilerplate? %> -# Limit actions available to your users by adding them to the 'except' array -# actions :all, except: [] - -# Add or remove filters (you can use any ActiveRecord scope) to toggle their -# visibility in the sidebar -<%= @boilerplate.filters %> - -# Add or remove columns to toggle their visiblity in the index action -# index do -# selectable_column -# id_column -<%= @boilerplate.columns %> -# actions -# end - -# Add or remove rows to toggle their visiblity in the show action -# show do |<%= class_name.downcase %>| -<%= @boilerplate.rows %> -# end - -# Add or remove fields to toggle their visibility in the form -<% end %> -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/lib/ransack_ext.rb b/lib/ransack_ext.rb deleted file mode 100644 index e73b92a61f0..00000000000 --- a/lib/ransack_ext.rb +++ /dev/null @@ -1,12 +0,0 @@ -# This sets up aliases for old Metasearch query methods so they behave -# identically to the versions given in Ransack. -# -Ransack.configure do |config| - {'contains'=>'cont', 'starts_with'=>'start', 'ends_with'=>'end'}.each do |old,current| - config.add_predicate old, Ransack::Constants::DERIVED_PREDICATES.detect{ |q, _| q == current }[1] - end - - {'equals'=>'eq', 'greater_than'=>'gt', 'less_than'=>'lt'}.each do |old,current| - config.add_predicate old, arel_predicate: current - end -end diff --git a/package.json b/package.json new file mode 100644 index 00000000000..859697bd979 --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "@activeadmin/activeadmin", + "version": "4.0.0-beta21", + "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": { + "@rollup/plugin-alias": "^6.0.0", + "eslint": "^9.39.1", + "gherkin-lint": "^4.2.4", + "rollup": "^4.56.0", + "tailwindcss": "^4.1.18", + "vitepress": "^1.6.4" + }, + "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: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 8c4081d142b..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! - -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 --skip-bundle" -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/travis_cache b/script/travis_cache deleted file mode 100755 index caec5a6226f..00000000000 --- a/script/travis_cache +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env ruby - -require 'base64' -require 'digest' -require 'openssl' -require 'shellwords' - -prefix = "ruby_#{RUBY_VERSION}-rails_#{ENV.fetch 'RAILS'}-" -BUNDLE = prefix + 'bundle' -APP = prefix + 'app' - -def download_bundle - s3 :save, file: "#{BUNDLE}.sha2", as: "remote-#{BUNDLE}.sha2" - s3 :save, file: "#{BUNDLE}.tgz", as: "remote-#{BUNDLE}.tgz", untar: true -end - -def download_app - # Force test app re-build if dependencies were updated - unless digest_changed? 'Gemfile.lock', "remote-#{BUNDLE}.sha2", "#{BUNDLE}.sha2" - s3 :save, file: "#{APP}.sha2", as: "remote-#{APP}.sha2" - # Force test app re-build if spec/support changed - unless digest_changed? 'spec/support', "remote-#{APP}.sha2", "#{APP}.sha2" - s3 :save, file: "#{APP}.tgz", as: "remote-#{APP}.tgz", untar: true - end - end -end - -def upload - unless ID && SECRET - puts "S3 credentials missing" - return - end - - [ ['Gemfile.lock', BUNDLE, 'bundle'], - ['spec/support', APP, 'spec/rails'] - ].each do |to_check, name, to_save| - puts "=> Checking #{to_check} for changes" - if ret = digest_changed?(to_check, "remote-#{name}.sha2", "#{name}.sha2") - puts " => Changes found: #{ret[:old]} -> #{ret[:new]}" - - puts " => Creating an archive" - `tar -cjf #{name}.tgz #{to_save}` - - puts " => Uploading a new archive" - s3 :upload, file: "#{name}.tgz" - s3 :upload, file: "#{name}.sha2" - else - puts " => There were no changes, doing nothing" - end - end -end - -def digest_changed?(to_check, old_digest_file, new_digest_file) - if File.exists? new_digest_file - digest = File.read new_digest_file - else - # Supports a single file, as well as a folder - files = Dir[to_check, "#{to_check}/**/*"].reject{ |f| File.directory? f } - content = files.sort!.map{ |f| File.read f }.join - digest = Digest::SHA2.hexdigest content - File.write new_digest_file, digest - end - - old_digest = File.read old_digest_file if File.exists? old_digest_file - - {old: old_digest, new: digest} if digest != old_digest -end - -def sign(secret, to_sign) - Base64.strict_encode64 OpenSSL::HMAC.digest OpenSSL::Digest::SHA1.new, secret, to_sign -end - -ID = ENV['AWS_S3_ID'] -SECRET = ENV['AWS_S3_SECRET'] -URL = 'https://s3.amazonaws.com/ActiveAdmin' - -# s3 :list -# s3 :upload, file: 'foo' -# s3 :find, file: 'foo' -# s3 :save, file: 'foo', as: 'bar' -# s3 :save, file: 'foo', untar: true -def s3(action, options = {}) - verb = {list: :get, upload: :put, find: :get, save: :get}.fetch action - file = options.fetch(:file) unless action == :list - extra_arg = case action - when :upload then "-T #{file}" - when :save then options[:as] ? "-o #{options[:as]}" : '-O' - end - do_after = "&& tar -xf #{options[:as] || file}" if options[:untar] - - if ID && SECRET - now = Time.now.strftime "%a, %d %b %Y %H:%M:%S %z" - signature = sign SECRET, "#{verb.upcase}\n\n\n#{now}\n/ActiveAdmin/#{file}" - headers = ["Authorization: AWS #{ID}:#{signature}", "Date: #{now}"].map do |h| - "-H #{Shellwords.escape h}" - end.join ' ' - end - - output = `curl -f #{headers} #{extra_arg} #{URL}/#{file} #{do_after}` - [$?.success?, output] -end - -if %w[download_bundle download_app upload].include? ARGV[0] - send ARGV[0] -else - raise "unexpected argument(s): #{ARGV}" -end 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/unit/view_helpers/breadcrumbs_spec.rb b/spec/helpers/breadcrumb_helper_spec.rb similarity index 64% rename from spec/unit/view_helpers/breadcrumbs_spec.rb rename to spec/helpers/breadcrumb_helper_spec.rb index e83fc9f1e3f..13c05201ac7 100644 --- a/spec/unit/view_helpers/breadcrumbs_spec.rb +++ b/spec/helpers/breadcrumb_helper_spec.rb @@ -1,28 +1,33 @@ -require 'rails_helper' - -describe "Breadcrumbs" do - - include ActiveAdmin::ViewHelpers +# 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 } - def params; {}; end - def link_to(name, url); {name: name, path: url}; end - - actions = ActiveAdmin::BaseController::ACTIVE_ADMIN_ACTIONS - - let(:user) { double display_name: 'Jane Doe' } - let(:user_config) { double find_resource: user, resource_name: double(route_key: 'users'), - defined_actions: actions } - let(:post) { double display_name: 'Hello World' } - let(:post_config) { double find_resource: post, resource_name: double(route_key: 'posts'), - defined_actions: actions, belongs_to_config: double(target: user_config) } + 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) { breadcrumb_links(path) } + 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" } @@ -33,14 +38,15 @@ def link_to(name, url); {name: name, path: url}; end end context "when path 'admin/users'" do - let(:path) { 'admin/users' } + let(:path) { "admin/users" } - it 'should have one item' do + 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' + + it "should have a link to /admin" do + expect(trail[0][:name]).to eq "Admin" + expect(trail[0][:path]).to eq "/admin" end end @@ -50,6 +56,7 @@ def link_to(name, url); {name: name, path: url}; end 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" @@ -62,10 +69,12 @@ def link_to(name, url); {name: name, path: url}; end 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" @@ -78,10 +87,12 @@ def link_to(name, url); {name: name, path: url}; end 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" @@ -109,10 +120,12 @@ def link_to(name, url); {name: name, path: url}; end 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" @@ -128,8 +141,8 @@ def link_to(name, url); {name: name, path: url}; 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) + 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 :)" @@ -138,52 +151,98 @@ def link_to(name, url); {name: name, path: url}; end end end - context "when path '/admin/users/1/coments/1'" do + 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/coments/1/edit'" do + 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" @@ -191,27 +250,32 @@ def link_to(name, url); {name: name, path: url}; end end context "when the 'show' action is disabled" do - let(:post_config) { double find_resource: post, resource_name: double(route_key: 'posts'), - defined_actions: actions - [:show], # this is the change - belongs_to_config: double(target: user_config) } + 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..1cf27aa7b89 --- /dev/null +++ b/spec/helpers/filter_form_helper_spec.rb @@ -0,0 +1,582 @@ +# 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 + 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 "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 + 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 + 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 +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..c01a36e22b4 --- /dev/null +++ b/spec/helpers/layout_helper_spec.rb @@ -0,0 +1,69 @@ +# 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 "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/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 9788883b634..00000000000 --- a/spec/javascripts/coffeescripts/jquery.aa.checkbox-toggler-spec.js.coffee +++ /dev/null @@ -1,50 +0,0 @@ -describe "ActiveAdmin.CheckboxToggler", -> - - beforeEach -> - loadFixtures('checkboxes.html') - - @collection = $("#collection") - @toggle_all = @collection.find(".toggle_all") - - @checkboxes = @collection.find(":checkbox").not(@toggle_all) - - - new ActiveAdmin.CheckboxToggler({}, @collection) - - describe "on init", -> - it "should raise an error if container not found", -> - expect( => new ActiveAdmin.CheckboxToggler({}) ).toThrow("Container element not found") - - it "should raise an error if 'toggle all' checkbox not found", -> - @toggle_all.remove() - expect( => new ActiveAdmin.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.flash.js.coffee b/spec/javascripts/coffeescripts/jquery.aa.flash.js.coffee deleted file mode 100644 index 34726aaaa64..00000000000 --- a/spec/javascripts/coffeescripts/jquery.aa.flash.js.coffee +++ /dev/null @@ -1,25 +0,0 @@ -describe "ActiveAdmin.flash", -> - beforeEach -> - loadFixtures('flashes.html') - @flashes = $(".flashes") - - describe "abstract", -> - it "should add a flash box with class and message" - ActiveAdmin.flash.abstract "a abstract message", "abstract" - flash = @flashes.find(".flash") - expect(flash).toHaveClass("abstract") - expect(flash.text()).toHaveClass("a abstract message") - - describe "error", -> - it "should add a flash box with class and message" - ActiveAdmin.flash.abstract "a error message" - flash = @flashes.find(".flash") - expect(flash).toHaveClass("error") - expect(flash.text()).toHaveClass("a error message") - - describe "notice", -> - it "should add a flash box with class and message" - ActiveAdmin.flash.abstract "a notice message" - flash = @flashes.find(".flash") - expect(flash).toHaveClass("notice") - expect(flash.text()).toHaveClass("a notice message") 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 8378d4aa9be..00000000000 --- a/spec/javascripts/coffeescripts/jquery.aa.table-checkbox-toggler-spec.js.coffee +++ /dev/null @@ -1,34 +0,0 @@ -describe "ActiveAdmin.TableCheckboxToggler", -> - - beforeEach -> - loadFixtures('table_checkboxes.html'); - - @collection = $("#collection") - @toggle_all = @collection.find(".toggle_all") - - @checkboxes = @collection.find(":checkbox").not(@toggle_all) - - new ActiveAdmin.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/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/flashes.html b/spec/javascripts/fixtures/flashes.html deleted file mode 100644 index 7559016977f..00000000000 --- a/spec/javascripts/fixtures/flashes.html +++ /dev/null @@ -1,2 +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/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/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 index 2a9367dbadf..a64c0e3034d 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,158 +1,29 @@ -require 'spec_helper' +# frozen_string_literal: true +require "spec_helper" -module ActiveAdminIntegrationSpecHelper - extend self +ENV["RAILS_ENV"] = "test" - def load_defaults! - ActiveAdmin.unload! - ActiveAdmin.load! - ActiveAdmin.register(Category) - ActiveAdmin.register(User) - ActiveAdmin.register(Post){ belongs_to :user, optional: true } - reload_menus! - end +require_relative "../tasks/test_application" - def reload_menus! - ActiveAdmin.application.namespaces.each{|n| n.reset_menu! } - end +require "#{ActiveAdmin::TestApplication.new.full_app_dir}/config/environment.rb" - # 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 - - def with_translation(translation) - I18n.backend.store_translations :en, translation - yield - ensure - I18n.backend.reload! - 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 - -require 'rails' -require 'active_record' -require 'active_admin' -require 'devise' -ActiveAdmin.application.load_paths = [ENV['RAILS_ROOT'] + "/app/admin"] - -require ENV['RAILS_ROOT'] + '/config/environment' - -require 'rspec/rails' - -# Prevent Test::Unit's AutoRunner from executing during RSpec's rake task on -# JRuby -Test::Unit.run = true if defined?(Test::Unit) && Test::Unit.respond_to?(:run=) - -# Setup Some Admin stuff for us to play with -include ActiveAdminIntegrationSpecHelper -load_defaults! -reload_routes! +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 -# Don't add asset cache timestamps. Makes it easy to integration -# test for the presence of an asset file -ENV["RAILS_ASSET_ID"] = '' - RSpec.configure do |config| config.use_transactional_fixtures = true config.use_instantiated_fixtures = false - config.include Devise::TestHelpers, type: :controller config.render_views = false - config.filter_run focus: true - config.filter_run_excluding skip: true - config.run_all_when_everything_filtered = true -end - -# All RSpec configuration needs to happen before any examples -# or else it whines. -require "support/active_admin_request_helpers" -RSpec.configure do |c| - c.include ActiveAdminRequestHelpers, type: :request - c.include Devise::TestHelpers, type: :controller -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 + config.include Devise::Test::ControllerHelpers, type: :controller -# 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 + 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 index ec1dafbe902..527f2c12a99 100644 --- a/spec/requests/default_namespace_spec.rb +++ b/spec/requests/default_namespace_spec.rb @@ -1,24 +1,17 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Application, :type => :request do - - include Rails.application.routes.url_helpers +RSpec.describe ActiveAdmin::Application, type: :request do + let(:resource) { ActiveAdmin.register Category } [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! + around do |example| + with_custom_default_namespace(value) { example.call } end - after(:all) do - ActiveAdmin.application = @__original_application + it "should generate resource paths" do + expect(resource.route_collection_path).to eq "/categories" end it "should generate a log out path" do @@ -28,34 +21,63 @@ 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 - end + it "should generate resource paths" do + expect(resource.route_collection_path).to eq "/test/categories" + end - describe "with a test default namespace" do + it "should generate a log out path" do + expect(destroy_admin_user_session_path).to eq "/test/logout" + end - before(:all) do - @__original_application = ActiveAdmin.application - application = ActiveAdmin::Application.new - application.default_namespace = :test - ActiveAdmin.application = application - load_defaults! - reload_routes! - end + it "should generate a log in path" do + expect(new_admin_user_session_path).to eq "/test/login" + end + end - after(:all) do - ActiveAdmin.application = @__original_application - 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 a log out path" do - expect(destroy_admin_user_session_path).to eq "/test/logout" - end + it "should generate resource paths" do + expect(resource.route_collection_path).to eq "/abc_123/categories" + end - it "should generate a log in path" do - expect(new_admin_user_session_path).to eq "/test/login" - 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/javascript_spec.rb b/spec/requests/javascript_spec.rb deleted file mode 100644 index d8099b065a8..00000000000 --- a/spec/requests/javascript_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'rails_helper' -require 'jslint' - -%x[which java] -if $? == 0 # Only run the JS Lint test if Java is installed - describe "Javascript", :type => :request 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/requests/memory_spec.rb b/spec/requests/memory_spec.rb index 4594f513f85..08c1fb5b907 100644 --- a/spec/requests/memory_spec.rb +++ b/spec/requests/memory_spec.rb @@ -1,8 +1,13 @@ -require 'rails_helper' +# 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 -describe "Memory Leak", type: :request, if: RUBY_ENGINE == 'ruby' do def count_instances_of(klass) - ObjectSpace.each_object(klass) { } + ObjectSpace.each_object(klass) {} end [ActiveAdmin::Namespace, ActiveAdmin::Resource].each do |klass| @@ -11,7 +16,7 @@ def count_instances_of(klass) GC.start count = count_instances_of(klass) - load_defaults! + load_resources { ActiveAdmin.register(Category) } GC.start GC.disable if previously_disabled diff --git a/spec/requests/stylesheets_spec.rb b/spec/requests/stylesheets_spec.rb deleted file mode 100644 index 52437a61343..00000000000 --- a/spec/requests/stylesheets_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'rails_helper' - -describe "Stylesheets", :type => :request do - - require "sprockets" - - let(:css) do - assets = Rails.application.assets - assets.find_asset("active_admin.css") - end - it "should successfully render the scss stylesheets using sprockets" do - expect(css).to_not be_nil - end - it "should not have any syntax errors" do - expect(css.to_s).to_not include("Syntax error:") - end - -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5b9f32cadf1..473ff525395 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,17 +1,16 @@ -$LOAD_PATH.unshift(File.dirname(__FILE__)) -$LOAD_PATH << File.expand_path('../support', __FILE__) +# frozen_string_literal: true +require "simplecov" if ENV["COVERAGE"] == "true" -ENV['BUNDLE_GEMFILE'] = File.expand_path('../../Gemfile', __FILE__) -require "bundler" -Bundler.setup +require_relative "support/matchers/perform_database_query_matcher" +require_relative "support/shared_contexts/capture_stderr" +require_relative "support/active_support_deprecation" -require 'detect_rails_version' -ENV['RAILS'] = detect_rails_version - -require 'simplecov' - -SimpleCov.start do - add_filter 'spec/' - add_filter 'features/' - add_filter 'bundle/' # for Travis +RSpec.configure do |config| + 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/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_admin_request_helpers.rb b/spec/support/active_admin_request_helpers.rb deleted file mode 100644 index 383f4bf26a8..00000000000 --- a/spec/support/active_admin_request_helpers.rb +++ /dev/null @@ -1,27 +0,0 @@ -require "action_dispatch" -require "capybara/rails" -require "capybara/dsl" - -module ActiveAdminRequestHelpers - 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 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 59cde7ff8c8..00000000000 --- a/spec/support/detect_rails_version.rb +++ /dev/null @@ -1,34 +0,0 @@ -# Detects the current version of Rails that is being used -# -# -RAILS_VERSION_FILE ||= File.expand_path("../../../.rails-version", __FILE__) - -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']['matrix'].grep(/RAILS=(.*)/){ $1 } -end - -DEFAULT_RAILS_VERSION ||= TRAVIS_RAILS_VERSIONS.last - -def detect_rails_version - version = version_from_file || ENV['RAILS'] || DEFAULT_RAILS_VERSION -ensure - puts "Detected Rails: #{version}" if ENV['DEBUG'] -end - -def detect_rails_version! - detect_rails_version or raise "can't find a version of Rails to use!" -end - -def version_from_file - if File.exists?(RAILS_VERSION_FILE) - version = File.read(RAILS_VERSION_FILE).chomp.strip - version unless version == '' - end -end - -def write_rails_version(version) - File.open(RAILS_VERSION_FILE, "w+"){|f| f << version } -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 056932c6414..767c5b95daa 100644 --- a/spec/support/rails_template.rb +++ b/spec/support/rails_template.rb @@ -1,145 +1,129 @@ +# frozen_string_literal: true # Rails template to build the sample app for specs -run "rm Gemfile" -run "rm -r test" +gem "cssbundling-rails" -# 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" +# Manifest already exists in Rails 7.2 +create_file "app/assets/config/manifest.js", skip: true -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" +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" -if File.exists? 'config/secrets.yml' - gsub_file 'config/secrets.yml', /\z/, "\ncucumber:\n secret_key_base: #{'o' * 128}" - gsub_file 'config/secrets.yml', /\z/, "\ncucumber_with_reloading:\n secret_key_base: #{'o' * 128}" -end +rails_command "importmap:install" -generate :model, "post title:string body:text published_at:datetime author_id:integer position:integer custom_category_id:integer starred:boolean foo_id:integer" -inject_into_file 'app/models/post.rb', %q{ - 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 - attr_accessible :author, :position unless Rails::VERSION::MAJOR > 3 && !defined? ProtectedAttributes -}, after: 'class Post < ActiveRecord::Base' -copy_file File.expand_path('../templates/post_decorator.rb', __FILE__), "app/models/post_decorator.rb" - -generate :model, "blog/post title:string body:text published_at:datetime author_id:integer position:integer custom_category_id:integer starred:boolean foo_id:integer" -inject_into_file 'app/models/blog/post.rb', %q{ - 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 - attr_accessible :author, :position unless Rails::VERSION::MAJOR > 3 && !defined? ProtectedAttributes -}, after: 'class Blog::Post < ActiveRecord::Base' - - -generate :model, "profile user_id:integer bio:text" -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' - has_one :profile - accepts_nested_attributes_for :profile, allow_destroy: true - def display_name - "#{first_name} #{last_name}" - end -}, after: 'class User < ActiveRecord::Base' - -inject_into_file 'app/models/profile.rb', %q{ - belongs_to :user -}, after: 'class Profile < 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, foreign_key: :custom_category_id - has_many :authors, through: :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' +initial_timestamp = Time.now.strftime("%Y%m%d%H%M%S").to_i -generate :model, "tagging post_id:integer tag_id:integer" -inject_into_file 'app/models/tagging.rb', %q{ - belongs_to :post - belongs_to :tag -}, after: 'class Tagging < ActiveRecord::Base' +template File.expand_path("templates/migrations/create_posts.tt", __dir__), "db/migrate/#{initial_timestamp}_create_posts.rb" -# 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" +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" -# 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__)" -inject_into_file "config/application.rb", "\nrequire 'devise'\n", after: "require 'rails/all'" +template File.expand_path("templates/migrations/create_blog_posts.tt", __dir__), "db/migrate/#{initial_timestamp + 1}_create_blog_posts.rb" -# Force strong parameters to raise exceptions -inject_into_file 'config/application.rb', "\n\n config.action_controller.action_on_unpermitted_parameters = :raise if Rails::VERSION::MAJOR == 4\n\n", after: 'class Application < Rails::Application' +copy_file File.expand_path("templates/models/blog/post.rb", __dir__), "app/models/blog/post.rb" -# Add some translations -append_file "config/locales/en.yml", File.read(File.expand_path('../templates/en.yml', __FILE__)) +template File.expand_path("templates/migrations/create_profiles.tt", __dir__), "db/migrate/#{initial_timestamp + 2}_create_profiles.rb" -# Add predefined admin resources -directory File.expand_path('../templates/admin', __FILE__), "app/admin" +copy_file File.expand_path("templates/models/user.rb", __dir__), "app/models/user.rb" -# Add predefined policies -directory File.expand_path('../templates/policies', __FILE__), 'app/policies' +template File.expand_path("templates/migrations/create_users.tt", __dir__), "db/migrate/#{initial_timestamp + 3}_create_users.rb" -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +copy_file File.expand_path("templates/models/profile.rb", __dir__), "app/models/profile.rb" -generate 'active_admin:install' +copy_file File.expand_path("templates/models/publisher.rb", __dir__), "app/models/publisher.rb" -inject_into_file "config/routes.rb", "\n root to: redirect('/admin')", after: /.*::Application.routes.draw do/ -remove_file "public/index.html" if File.exists? "public/index.html" +template File.expand_path("templates/migrations/create_categories.tt", __dir__), "db/migrate/#{initial_timestamp + 4}_create_categories.rb" -# Devise master doesn't set up its secret key on Rails 4.1 -# https://github.com/plataformatec/devise/issues/2554 -gsub_file 'config/initializers/devise.rb', /# config.secret_key =/, 'config.secret_key =' +copy_file File.expand_path("templates/models/category.rb", __dir__), "app/models/category.rb" -rake "db:migrate db:test:prepare" -run "/usr/bin/env RAILS_ENV=cucumber rake db:migrate" +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" -if ENV['INSTALL_PARALLEL'] - inject_into_file 'config/database.yml', "<%= ENV['TEST_ENV_NUMBER'] %>", after: 'test.sqlite3' - inject_into_file 'config/database.yml', "<%= ENV['TEST_ENV_NUMBER'] %>", after: 'cucumber.sqlite3', force: true +template File.expand_path("templates/migrations/create_tags.tt", __dir__), "db/migrate/#{initial_timestamp + 6}_create_tags.rb" - # 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) - end +copy_file File.expand_path("templates/models/tag.rb", __dir__), "app/models/tag.rb" - 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) +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/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 "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) + 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 + +# 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" + +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 56f6d60885b..1923503af6f 100644 --- a/spec/support/rails_template_with_data.rb +++ b/spec/support/rails_template_with_data.rb @@ -1,55 +1,94 @@ -apply File.expand_path("../rails_template.rb", __FILE__) +# frozen_string_literal: true +apply File.expand_path("rails_template.rb", __dir__) -%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|" -inject_into_file 'app/admin/post.rb', <<-RUBY, after: "ActiveAdmin.register Post do\n" - 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 -RUBY +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." + ] -append_file "db/seeds.rb", "\n\n" + <<-RUBY.strip_heredoc - 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 + 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..6beaa24371f --- /dev/null +++ b/spec/support/simplecov_changes_env.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +if ENV["COVERAGE"] == "true" + require "simplecov" + + SimpleCov.command_name "filesystem changes specs" +end diff --git a/spec/support/simplecov_regular_env.rb b/spec/support/simplecov_regular_env.rb new file mode 100644 index 00000000000..a02f9ffc76c --- /dev/null +++ b/spec/support/simplecov_regular_env.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +if ENV["COVERAGE"] == "true" + require "simplecov" + + SimpleCov.command_name ["regular specs", ENV["TEST_ENV_NUMBER"]].join(" ").rstrip +end 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 daddb7234b3..50820770d1c 100644 --- a/spec/support/templates/admin/stores.rb +++ b/spec/support/templates/admin/stores.rb @@ -1,9 +1,6 @@ +# frozen_string_literal: true ActiveAdmin.register Store do - - if Rails::VERSION::MAJOR == 4 - permit_params :name - end + 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/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..17eb1302e3a --- /dev/null +++ b/spec/support/templates/models/user.rb @@ -0,0 +1,18 @@ +# 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_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 index 948e7e97379..8586202b6b5 100644 --- a/spec/support/templates/policies/active_admin/comment_policy.rb +++ b/spec/support/templates/policies/active_admin/comment_policy.rb @@ -1,9 +1,14 @@ +# frozen_string_literal: true module ActiveAdmin class CommentPolicy < ApplicationPolicy - class Scope < Struct.new(:user, :scope) + def destroy? + record.author == user + end + + class Scope < ApplicationPolicy::Scope def resolve - scope + scope.where(author: user) end end end -end \ No newline at end of file +end diff --git a/spec/support/templates/policies/active_admin/page_policy.rb b/spec/support/templates/policies/active_admin/page_policy.rb index 0f20fbead53..f691f6fac62 100644 --- a/spec/support/templates/policies/active_admin/page_policy.rb +++ b/spec/support/templates/policies/active_admin/page_policy.rb @@ -1,11 +1,6 @@ +# frozen_string_literal: true module ActiveAdmin class PagePolicy < ApplicationPolicy - class Scope < Struct.new(:user, :scope) - def resolve - scope - end - end - def show? case record.name when "Dashboard" @@ -15,4 +10,4 @@ def show? end end end -end \ No newline at end of file +end diff --git a/spec/support/templates/policies/admin_user_policy.rb b/spec/support/templates/policies/admin_user_policy.rb index 27a63101108..c534681af69 100644 --- a/spec/support/templates/policies/admin_user_policy.rb +++ b/spec/support/templates/policies/admin_user_policy.rb @@ -1,11 +1,3 @@ +# frozen_string_literal: true class AdminUserPolicy < ApplicationPolicy - class Scope < Struct.new(:user, :scope) - def resolve - scope - end - end - - def destroy? - record != user - end end diff --git a/spec/support/templates/policies/application_policy.rb b/spec/support/templates/policies/application_policy.rb index 3912a49e151..a95f6fad5fb 100644 --- a/spec/support/templates/policies/application_policy.rb +++ b/spec/support/templates/policies/application_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class ApplicationPolicy attr_reader :user, :record @@ -11,7 +12,7 @@ def index? end def show? - scope.where(:id => record.id).exists? + scope.where(id: record.id).exists? end def new? @@ -38,8 +39,20 @@ def destroy_all? true end - def scope Pundit.policy_scope!(user, record.class) end -end \ No newline at end of file + + 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 index a3035d24485..3aef6aa9358 100644 --- a/spec/support/templates/policies/category_policy.rb +++ b/spec/support/templates/policies/category_policy.rb @@ -1,7 +1,3 @@ +# frozen_string_literal: true class CategoryPolicy < ApplicationPolicy - class Scope < Struct.new(:user, :scope) - def resolve - scope - end - end 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 index e03f39ea308..aeb5424de63 100644 --- a/spec/support/templates/policies/post_policy.rb +++ b/spec/support/templates/policies/post_policy.rb @@ -1,15 +1,14 @@ +# frozen_string_literal: true class PostPolicy < ApplicationPolicy - class Scope < Struct.new(:user, :scope) - def resolve - scope - end + def new? + true end - def update? - record.author == user + def create? + record.category.nil? || record.category.name != "Announcements" || user.is_a?(User::VIP) end - def destroy? - update? + 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 index ae734d48541..2038c15c661 100644 --- a/spec/support/templates/policies/store_policy.rb +++ b/spec/support/templates/policies/store_policy.rb @@ -1,11 +1,3 @@ +# frozen_string_literal: true class StorePolicy < ApplicationPolicy - class Scope < Struct.new(:user, :scope) - def resolve - scope - end - end - - def destroy? - false - end 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 index 6edfa9ec3b2..cb5a7e4b5c9 100644 --- a/spec/support/templates/policies/user_policy.rb +++ b/spec/support/templates/policies/user_policy.rb @@ -1,11 +1,3 @@ +# frozen_string_literal: true class UserPolicy < ApplicationPolicy - class Scope < Struct.new(:user, :scope) - def resolve - scope - end - end - - def destroy_all? - true - end end diff --git a/spec/support/templates/post_decorator.rb b/spec/support/templates/post_decorator.rb index 786231a6988..2548d0a2b9b 100644 --- a/spec/support/templates/post_decorator.rb +++ b/spec/support/templates/post_decorator.rb @@ -1,11 +1,25 @@ -require 'draper' +# frozen_string_literal: true +require "draper" class PostDecorator < Draper::Decorator decorates :post delegate_all + # @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 virtual_title + object.title + end + + def virtual_title=(virtual_title) + object.title = virtual_title + end + def decorator_method - 'A method only available on the decorator' + "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..01eccbf9740 --- /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/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..cf985f25169 --- /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` 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 8a9bb0c9b8b..00000000000 --- a/spec/unit/abstract_view_factory_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -require 'rails_helper' - -require 'active_admin/abstract_view_factory' - -describe ActiveAdmin::AbstractViewFactory do - - let(:view_factory){ ActiveAdmin::AbstractViewFactory.new } - let(:view){ Class.new } - - describe "registering a new view key" do - before do - view_factory.register my_new_view_class: view - end - - it "should respond to :my_new_view_class" do - expect(view_factory.respond_to? :my_new_view_class).to be true - end - - it "should respond to :my_new_view_class=" do - expect(view_factory.respond_to? :my_new_view_class=).to be true - end - - it "should generate a getter method" do - expect(view_factory.my_new_view_class).to eq view - end - - it "should be settable view a setter method" do - view_factory.my_new_view_class = "Some Obj" - expect(view_factory.my_new_view_class).to eq "Some Obj" - end - end - - describe "array syntax access" do - before do - view_factory.register my_new_view_class: view - end - - it "should be available through array syntax" do - expect(view_factory[:my_new_view_class]).to eq view - end - - it "should be settable through array syntax" do - view_factory[:my_new_view_class] = "My New View Class" - expect(view_factory[:my_new_view_class]).to eq "My New View Class" - end - end - - describe "registering default views" do - before do - ActiveAdmin::AbstractViewFactory.register my_default_view_class: view - end - it "should generate a getter method" do - expect(view_factory.my_default_view_class).to eq view - end - it "should be settable view a setter method and not change default" do - view_factory.my_default_view_class = "Some Obj" - expect(view_factory.my_default_view_class).to eq "Some Obj" - expect(view_factory.default_for(:my_default_view_class)).to eq 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 - expect(factory.my_subclassed_view).to eq "From Subclass" - end - end - - -end diff --git a/spec/unit/action_builder_spec.rb b/spec/unit/action_builder_spec.rb index 37b8a2b51e4..53d8e4ed3dd 100644 --- a/spec/unit/action_builder_spec.rb +++ b/spec/unit/action_builder_spec.rb @@ -1,17 +1,18 @@ -require 'rails_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,11 +25,13 @@ end it "should create a new public instance method" do - expect(controller.public_instance_methods.collect(&:to_s)).to include("comment") + expect(klass.public_instance_methods.collect(&:to_s)).to include("comment") end + it "should add itself to the member actions config" do - expect(controller.active_admin_config.member_actions.size).to eq 1 + expect(klass.active_admin_config.member_actions.size).to eq 1 end + it "should create a new named route" do expect(Rails.application.routes.url_helpers.methods.collect(&:to_s)).to include("comment_admin_post_path") end @@ -40,31 +43,32 @@ member_action :comment end end + it "should still generate a new empty action" do - expect(controller.public_instance_methods.collect(&:to_s)).to 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 { is_expected.to set_page_title_to "My Awesome Comment", for: controller } + 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,58 +79,74 @@ end end end - it "should create a new public instance method" do - expect(controller.public_instance_methods.collect(&:to_s)).to 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 - expect(controller.active_admin_config.collection_actions.size).to eq 1 + expect(klass.active_admin_config.collection_actions.size).to eq 1 end - it "should create a new named route" do + + 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 - expect(controller.public_instance_methods.collect(&:to_s)).to 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 { is_expected.to set_page_title_to "My Awesome Comments", for: controller } + expect(controller.instance_variable_get(:@page_title)).to eq "My Awesome Comments" + end end end - def find_before_filter(controller, action) - finder = if ActiveAdmin::Dependency.rails? '>= 4.1.0' - ->c { c.kind == :before && c.instance_variable_get(:@if) == ["action_name == '#{action}'"] } - else - ->c { c.kind == :before && c.options[:only] == [action] } - end + context "when method with given name is already defined" do + include_context "capture stderr" - controller._process_action_callbacks.detect &finder - end + 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, options| - match do |filter| - filter.raw_filter.call - @actual = options[:for].instance_variable_get(:@page_title) - expect(@actual).to eq expected + it "writes warning to $stderr" do + expect($stderr.string).to include("Warning: method `process` already defined in Admin::PostsController") + end end - failure_message 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 326c2da2f70..a3871bf8dc3 100644 --- a/spec/unit/active_admin_spec.rb +++ b/spec/unit/active_admin_spec.rb @@ -1,6 +1,7 @@ -require 'rails_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 expect(ActiveAdmin.application).to receive(method) diff --git a/spec/unit/application_spec.rb b/spec/unit/application_spec.rb index 59e7bcba1c4..455325b4de2 100644 --- a/spec/unit/application_spec.rb +++ b/spec/unit/application_spec.rb @@ -1,21 +1,20 @@ -require 'rails_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 - expect(application.load_paths).to eq [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 paths (Active Admin deals with loading)" do - expect(ActiveSupport::Dependencies.autoload_paths).to_not include(File.join(Rails.root, "app/admin")) + describe "#prepare" do + before { application.prepare! } + + 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 @@ -27,26 +26,9 @@ expect(application.site_title).to eq "New Title" end - it "should store the site's title link" do - expect(application.site_title_link).to eq "" - end - - it "should set the site's title link" do - application.site_title_link = "http://www.mygreatsite.com" - expect(application.site_title_link).to eq "http://www.mygreatsite.com" - end - - it "should store the site's title image" do - expect(application.site_title_image).to eq "" - 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" - expect(application.site_title_image).to eq "http://railscasts.com/assets/episodes/stills/284-active-admin.png?1316476106" - end - - it "should store the site's favicon" do - expect(application.favicon).to eq false + 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 return default localize format" do @@ -58,41 +40,25 @@ expect(application.localize_format).to eq :default end - it "should set the site's favicon" do - application.favicon = "/a/favicon.ico" - expect(application.favicon).to eq "/a/favicon.ico" - end - - it "should store meta tags" do - expect(application.meta_tags).to eq({}) - end - - it "should set meta tags" do - application.meta_tags = { author: "My Company" } - expect(application.meta_tags).to eq(author: "My Company") - end - - it "should contains robots meta tags by default" do - result = application.meta_tags_for_logged_out_pages - expect(result).to eq(robots: "noindex, nofollow") + it "should allow comments by default" do + expect(application.comments).to eq true end - it "should set meta tags for logged out pages" do - value = { author: "My Company" } - application.meta_tags_for_logged_out_pages = value - expect(application.meta_tags_for_logged_out_pages).to eq value + it "should have default order clause class" do + expect(application.order_clause).to eq ActiveAdmin::OrderClause end - it "should have a view factory" do - expect(application.view_factory).to be_an_instance_of(ActiveAdmin::ViewFactory) + it "should have default show_count for scopes" do + expect(application.scopes_show_count).to eq true end - it "should allow comments by default" do - expect(application.comments).to eq true + 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 expect(application.current_user_method).to eq false end @@ -104,27 +70,49 @@ it "should have a logout link path (Devise's default)" do expect(application.logout_link_path).to eq :destroy_admin_user_session_path end - - it "should have a logout link method (Devise's default)" do - expect(application.logout_link_method).to eq :get - 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 - expect(application.files).to 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) - expect(application.files).to 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| expect(ns.name).to eq :new_namespace @@ -137,12 +125,21 @@ end it "should yield an existing namespace" do - expect { + expect do application.namespace :admin do |ns| 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 @@ -157,9 +154,8 @@ it "finds or create the namespace and register the page to it" do namespace = double expect(application).to receive(:namespace).with("public").and_return namespace - expect(namespace).to receive(:register_page).with("My Page", {namespace: "public"}) + 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 441c8d999db..00000000000 --- a/spec/unit/asset_registration_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::AssetRegistration do - include ActiveAdmin::AssetRegistration - - before do - clear_stylesheets! - clear_javascripts! - end - - it "should register a stylesheet file" do - register_stylesheet "active_admin.css" - expect(stylesheets.length).to eq 1 - expect(stylesheets.keys.first).to eq "active_admin.css" - end - - it "should clear all existing stylesheets" do - register_stylesheet "active_admin.css" - expect(stylesheets.length).to eq 1 - clear_stylesheets! - expect(stylesheets).to be_empty - end - - it "should allow media option when registering stylesheet" do - register_stylesheet "active_admin.css", media: :print - expect(stylesheets.values.first[:media]).to eq :print - end - - it "shouldn't register a stylesheet twice" do - register_stylesheet "active_admin.css" - register_stylesheet "active_admin.css" - expect(stylesheets.length).to eq 1 - end - - it "should register a javascript file" do - register_javascript "active_admin.js" - expect(javascripts).to eq ["active_admin.js"].to_set - end - - it "should clear all existing javascripts" do - register_javascript "active_admin.js" - expect(javascripts).to eq ["active_admin.js"].to_set - clear_javascripts! - expect(javascripts).to be_empty - end - - it "shouldn't register a javascript twice" do - register_javascript "active_admin.js" - register_javascript "active_admin.js" - expect(javascripts.length).to eq 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 dfb3abd4d8c..a31a22f25ba 100644 --- a/spec/unit/authorization/authorization_adapter_spec.rb +++ b/spec/unit/authorization/authorization_adapter_spec.rb @@ -1,31 +1,25 @@ -require 'rails_helper' - -describe ActiveAdmin::AuthorizationAdapter do +# frozen_string_literal: true +require "rails_helper" +RSpec.describe ActiveAdmin::AuthorizationAdapter do let(:adapter) { ActiveAdmin::AuthorizationAdapter.new(double, double) } describe "#authorized?" do - it "should always return true" do - expect(adapter.authorized?(:read, "Resource")).to be_truthy + expect(adapter.authorized?(:read, "Resource")).to eq true end - end describe "#scope_collection" do - it "should return the collection unscoped" do 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(double, double) } it "should match against a class" do - expect(adapter.authorized?(:read, String)).to be_truthy + expect(adapter.authorized?(:read, String)).to eq true end - it 'should match against an instance' do - expect(adapter.authorized?(:read, "String")).to be_truthy + it "should match against an instance" do + expect(adapter.authorized?(:read, "String")).to eq true end - it 'should not match a different class' do - expect(adapter.authorized?(:read, Hash)).to be_falsey + 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 - expect(adapter.authorized?(:read, {})).to be_falsey + 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 f8b05bb7c3f..deac16823dd 100644 --- a/spec/unit/authorization/controller_authorization_spec.rb +++ b/spec/unit/authorization/controller_authorization_spec.rb @@ -1,39 +1,43 @@ -require 'rails_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 - expect(authorization).to receive(:authorized?).with(Auth::READ, Post).and_return true + expect(authorization).to receive(:authorized?).with(auth::READ, Post).and_return true get :index - expect(response).to be_success + expect(response).to be_successful end it "should authorize the new action" do - expect(authorization).to 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 - expect(response).to be_success + expect(response).to be_successful end it "should authorize the create action with the new resource" do - expect(authorization).to 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 - expect(response).to 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 - expect(authorization).to receive(:authorized?).with(Auth::READ, Post).and_return false + expect(authorization).to receive(:authorized?).with(auth::READ, Post).and_return false get :index - expect(response.body).to eq 'You are being
    redirected.' - expect(response).to 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 index cfcc982a9c4..5b28a59f58f 100644 --- a/spec/unit/authorization/index_overriding_spec.rb +++ b/spec/unit/authorization/index_overriding_spec.rb @@ -1,22 +1,23 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe Admin::PostsController, 'Index overriding', type: :controller do +RSpec.describe "Index overriding", type: :controller do before do - controller.instance_eval do + load_resources { ActiveAdmin.register Post } + @controller = Admin::PostsController.new + + @controller.instance_eval do def index super do - render text: 'Rendered from passed block' and return + render body: "Rendered from passed block" + return end end end - 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 end - it 'should call block passed to overridden index' do + it "should call block passed to overridden index" do get :index - expect(response.body).to eq 'Rendered from passed block' + 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 822efc0b1eb..00000000000 --- a/spec/unit/auto_link_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -require 'rails_helper' - -describe "auto linking resources" do - include ActiveAdmin::ViewHelpers::ActiveAdminApplicationHelper - include ActiveAdmin::ViewHelpers::AutoLinkHelper - include ActiveAdmin::ViewHelpers::DisplayHelper - include MethodOrProcHelper - - let(:active_admin_config) { double namespace: 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 - - def authorized?(_action, _subject) - true - end - - context "when the resource is not registered" do - it "should return the display name of the object" do - expect(auto_link(post)).to eq "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 - expect(self).to receive(:url_for) { |url| url } - expect(self).to receive(:link_to).with "Hello World", admin_post_path(post) - auto_link(post) - end - - context "but the user doesn't have access" do - it "should return the display name of the object" do - expect(self).to receive(:authorized?).twice.and_return(false) - expect(auto_link(post)).to eq "Hello World" - end - end - - context "but the show action is disabled" do - before do - active_admin_namespace.register(Post) { actions :all, except: :show } - end - - it "should fallback to edit" do - url_path = "/admin/posts/#{post.id}/edit" - expect(self).to receive(:url_for) { |url| url } - expect(self).to receive(:link_to).with "Hello World", url_path - auto_link(post) - end - end - - context "but the show and edit actions are disabled" do - before do - active_admin_namespace.register(Post) do - actions :all, except: [:show, :edit] - end - end - - it "should return the display name of the object" do - expect(auto_link(post)).to eq "Hello World" - end - end - end -end diff --git a/spec/unit/batch_actions/resource_spec.rb b/spec/unit/batch_actions/resource_spec.rb index bff65d12ac1..99b3c8488bb 100644 --- a/spec/unit/batch_actions/resource_spec.rb +++ b/spec/unit/batch_actions/resource_spec.rb @@ -1,7 +1,7 @@ -require 'rails_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,16 +9,13 @@ end describe "default action" do - it "should have the default action by default" do expect(resource.batch_actions.size).to eq 1 - expect(resource.batch_actions.first.sym == :destroy).to be_truthy + 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 @@ -35,13 +32,11 @@ end it "should store the block in the batch action" do - expect(resource.batch_actions.first.block).to_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 @@ -49,24 +44,9 @@ it "should allow for batch action removal" do expect(resource.batch_actions.size).to eq 0 end - - end - - describe "#batch_action_path" do - - it "returns the path as a symbol" do - expect(resource.batch_action_path).to eq "/admin/posts/batch_action" - end - - it "includes :scope and :q params" do - params = { q: { name_equals: "Any" }, scope: :all } - batch_action_path = "/admin/posts/batch_action?q%5Bname_equals%5D=Any&scope=all" - expect(resource.batch_action_path(params)).to eq(batch_action_path) - end end describe "#display_if_block" do - it "should return true by default" do action = ActiveAdmin::BatchAction.new :default, "Default" expect(action.display_if_block.call).to eq true @@ -76,11 +56,9 @@ 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" expect(action.priority).to eq 10 @@ -91,7 +69,5 @@ 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 7ca153dd541..6338b8fcd3a 100644 --- a/spec/unit/batch_actions/settings_spec.rb +++ b/spec/unit/batch_actions/settings_spec.rb @@ -1,6 +1,7 @@ -require 'rails_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,42 +9,42 @@ it "should be disabled globally by default" do # Note: the default initializer would set it to true - expect(app.batch_actions).to be_falsey - expect(ns.batch_actions).to be_falsey - expect(post_resource.batch_actions_enabled?).to be_falsey + 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 - expect(app.batch_actions).to be_truthy + expect(app.batch_actions).to eq true end it "should be an inheritable_setting" do app.batch_actions = true - expect(ns.batch_actions).to be_truthy + 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 - expect(app.batch_actions).to be_truthy - expect(ns.batch_actions).to be_falsey + expect(app.batch_actions).to eq true + expect(ns.batch_actions).to eq false end it "should be settable at the resource level" do - expect(post_resource.batch_actions_enabled?).to be_falsey + expect(post_resource.batch_actions_enabled?).to eq false post_resource.batch_actions = true - expect(post_resource.batch_actions_enabled?).to be_truthy + 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 - expect(post_resource.batch_actions_enabled?).to be_falsey + expect(post_resource.batch_actions_enabled?).to eq false expect(post_resource.batch_actions).to be_empty post_resource.batch_actions = true - expect(post_resource.batch_actions_enabled?).to be_truthy + expect(post_resource.batch_actions_enabled?).to eq true expect(post_resource.batch_actions).to_not be_empty end @@ -51,11 +52,11 @@ ns.batch_actions = true post_resource.batch_actions = true - expect(post_resource.batch_actions_enabled?).to be_truthy + expect(post_resource.batch_actions_enabled?).to eq true expect(post_resource.batch_actions).to_not be_empty post_resource.batch_actions = nil - expect(post_resource.batch_actions_enabled?).to be_truthy # inherited from namespace + 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 96cc12ab701..f507a0cb4a1 100644 --- a/spec/unit/belongs_to_spec.rb +++ b/spec/unit/belongs_to_spec.rb @@ -1,10 +1,17 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Resource::BelongsTo do +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(:user_config){ ActiveAdmin.register User } - let(:post_config){ ActiveAdmin.register Post do belongs_to :user end } - let(:belongs_to){ post_config.belongs_to_config } + 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 expect(belongs_to.owner).to eq post_config @@ -18,23 +25,23 @@ end context "when the resource has not been registered" do - let(:belongs_to){ ActiveAdmin::Resource::BelongsTo.new post_config, :missing } + let(:belongs_to) { ActiveAdmin::Resource::BelongsTo.new post_config, :missing } it "should raise a ActiveAdmin::BelongsTo::TargetNotFound" do - expect { + expect do belongs_to.target - }.to 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 do; end } + 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 do; 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 @@ -49,16 +56,20 @@ class Blog::Author describe "controller" do let(:controller) { post_config.controller.new } + let(:http_params) { { user_id: user.id } } + let(:user) { User.create! } + before do - user = User.create! - request = double 'Request', format: 'application/json' - allow(controller).to receive(:params) { {user_id: user.id} } - allow(controller).to receive(:request){ request } + 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 + + 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 + + it "should be able to build a new resource" do expect(controller.send :build_resource).to be_a Post end end diff --git a/spec/unit/cancan_adapter_spec.rb b/spec/unit/cancan_adapter_spec.rb index 675af3df3cc..59a8b8213b3 100644 --- a/spec/unit/cancan_adapter_spec.rb +++ b/spec/unit/cancan_adapter_spec.rb @@ -1,12 +1,11 @@ -require 'rails_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 :ability_class do Class.new do @@ -14,9 +13,9 @@ def initialize(user) can :read, Post + can :create, Post cannot :update, Post end - end end @@ -32,12 +31,15 @@ def initialize(user) 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 = 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 25fd4f8569d..42fbc8228e9 100644 --- a/spec/unit/comments_spec.rb +++ b/spec/unit/comments_spec.rb @@ -1,25 +1,57 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe "Comments" do +RSpec.describe "Comments" do let(:application) { ActiveAdmin::Application.new } describe ActiveAdmin::Comment do - subject(:comment){ ActiveAdmin::Comment.new } - - it "has valid Associations and Validations" do - expect(comment).to belong_to :resource - expect(comment).to belong_to :author - expect(comment).to validate_presence_of :resource - expect(comment).to validate_presence_of :body - expect(comment).to 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" } before do - @comment = ActiveAdmin::Comment.create! resource: post, + @comment = ActiveAdmin::Comment.create! author: user, + resource: post, body: "A Comment", namespace: namespace_name end @@ -30,7 +62,7 @@ it "should not return a comment for the same resource in a different namespace" do ActiveAdmin.application.namespaces[:public] = ActiveAdmin.application.namespaces[:admin] - expect(ActiveAdmin::Comment.find_for_resource_in_namespace(post, 'public')).to eq [] + expect(ActiveAdmin::Comment.find_for_resource_in_namespace(post, "public")).to eq [] ActiveAdmin.application.namespaces.instance_variable_get(:@namespaces).delete(:public) end @@ -39,14 +71,15 @@ expect(ActiveAdmin::Comment.find_for_resource_in_namespace(another_post, namespace_name)).to eq [] end - it "should return the most recent comment first" do - ActiveAdmin::Comment.class_eval { attr_accessible :created_at } if Rails::VERSION::MAJOR == 3 - another_comment = ActiveAdmin::Comment.create! resource: post, + 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! resource: post, + yet_another_comment = ActiveAdmin::Comment.create! author: user, + resource: post, body: "Yet Another Comment", namespace: namespace_name, created_at: @comment.created_at + 10.minutes @@ -58,42 +91,38 @@ expect(comments.last).to eq(another_comment) end - it "should return the correctly ordered comments" do - ActiveAdmin::Application.inheritable_setting( - :comments_order, "created_at DESC" - ) + context "when custom ordering configured" do + around do |example| + previous_order = ActiveAdmin.application.comments_order + ActiveAdmin.application.comments_order = "created_at DESC" - another_comment = ActiveAdmin::Comment.create!( - 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 + example.call - describe ".resource_id_cast" do - let(:post) { Post.create!(title: "Testing.") } - let(:namespace_name) { "admin" } + ActiveAdmin.application.comments_order = previous_order + end - it "should cast resource_id as string" do - comment = ActiveAdmin::Comment.create! resource: post, - body: "Another Comment", - namespace: namespace_name - expect(ActiveAdmin::Comment.resource_id_cast(comment).class).to eql String + 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_type" do let(:post) { Post.create!(title: "Testing.") } - let(:post_decorator) { double 'PostDecorator' } + let(:post_decorator) { double "PostDecorator" } before do allow(post_decorator).to receive(:model).and_return(post) @@ -104,7 +133,7 @@ let(:resource) { post_decorator } it "returns undeorated object class string" do - expect(ActiveAdmin::Comment.resource_type resource).to eql 'Post' + expect(ActiveAdmin::Comment.resource_type resource).to eql "Post" end end @@ -112,23 +141,18 @@ let(:resource) { post } it "returns object class string" do - expect(ActiveAdmin::Comment.resource_type resource).to eql 'Post' + expect(ActiveAdmin::Comment.resource_type resource).to eql "Post" end end end - describe ".resource_id_type" do - it "should be :string" do - expect(ActiveAdmin::Comment.resource_id_type).to eql :string - end - end - describe "Commenting on resource with string id" do let(:tag) { Tag.create!(name: "cooltags") } let(:namespace_name) { "admin" } it "should allow commenting" do comment = ActiveAdmin::Comment.create!( + author: user, resource: tag, body: "Another Comment", namespace: namespace_name) @@ -142,7 +166,8 @@ let(:namespace_name) { "admin" } it "should assign child class as commented resource" do - comment = ActiveAdmin::Comment.create!( + ActiveAdmin::Comment.create!( + author: user, resource: publisher, body: "Lorem Ipsum", namespace: namespace_name) @@ -155,17 +180,16 @@ 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.comments = true - expect(ns.comments?).to be_truthy + 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.comments = false - expect(ns.comments?).to be_falsey + expect(ns.comments?).to eq false end end end @@ -174,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) - expect(resource.comments).to be_nil + expect(resource.comments).to eq nil resource.comments = true - expect(resource.comments).to be_truthy + 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 - expect(resource.comments?).to be_falsey + expect(resource.comments?).to eq false end end end diff --git a/spec/unit/component_spec.rb b/spec/unit/component_spec.rb index efeade7edaa..83ecc88f48a 100644 --- a/spec/unit/component_spec.rb +++ b/spec/unit/component_spec.rb @@ -1,18 +1,19 @@ -require 'rails_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 expect(ActiveAdmin::Component.ancestors).to include(Arbre::HTML::Div) end it "should render to a div, even as a subclass" do - expect(component.tag_name).to eq '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 f4f9ccb2ce6..a9065bea2ee 100644 --- a/spec/unit/config_shared_examples.rb +++ b/spec/unit/config_shared_examples.rb @@ -1,4 +1,5 @@ -shared_examples_for "ActiveAdmin::Resource" do +# frozen_string_literal: true +RSpec.shared_examples_for "ActiveAdmin::Resource" do describe "namespace" do it "should return the namespace" do expect(config.namespace).to eq(namespace) @@ -32,16 +33,14 @@ describe "Menu" do describe "#menu_item_options" do - it "initializes a new menu item with defaults" do - expect(config.menu_item_options[:label].call).to eq(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" } expect(config.menu_item_options[:label]).to eq("Hello") end - end describe "#include_in_menu?" do @@ -54,6 +53,5 @@ 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 28ed564a008..c5d4e852d10 100644 --- a/spec/unit/controller_filters_spec.rb +++ b/spec/unit/controller_filters_spec.rb @@ -1,10 +1,11 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Application do - let(:application){ ActiveAdmin::Application.new } - let(:controllers){ application.controllers_for_filters } +RSpec.describe ActiveAdmin::Application do + let(:application) { ActiveAdmin::Application.new } + let(:controllers) { application.controllers_for_filters } - it 'controllers_for_filters' do + it "controllers_for_filters" do expect(application.controllers_for_filters).to eq [ ActiveAdmin::BaseController, ActiveAdmin::Devise::SessionsController, ActiveAdmin::Devise::PasswordsController, ActiveAdmin::Devise::UnlocksController, @@ -12,30 +13,16 @@ ] end - expected_actions = ( - prefixes = %w(skip append prepend) << nil - positions = %w(before around after) - suffixes = %w(filter) - base = %w(skip_filter) - if Rails::VERSION::MAJOR >= 4 - suffixes += %w(action) - base += %w(skip_action_callback) - end - - prefixes.each_with_object(base) do |prefix, stack| - positions.each do |position| - suffixes.each do |suffix| - stack << [prefix, position, suffix].compact.join("_").to_sym - end - end - end - ) - - expected_actions.each do |action| - it action do + %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(action).with(args) } - application.public_send action, args + 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 d20a3646214..95c3bd70854 100644 --- a/spec/unit/csv_builder_spec.rb +++ b/spec/unit/csv_builder_spec.rb @@ -1,45 +1,54 @@ -# Encoding: UTF-8 +# frozen_string_literal: true +require "rails_helper" -require 'rails_helper' +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 ActiveAdmin::CSVBuilder do - - describe '.default_for_resource using Post' do - let(:csv_builder) { ActiveAdmin::CSVBuilder.default_for_resource(Post).tap(&:exec_columns) } - - it 'returns a default csv_builder for Post' do + it "returns a default csv_builder for Post" do expect(csv_builder).to be_a(ActiveAdmin::CSVBuilder) end - it 'defines Id as the first column' do - expect(csv_builder.columns.first.name).to eq '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 it "has Post's content_columns" do csv_builder.columns[1..-1].each_with_index do |column, index| - expect(column.name).to eq Post.content_columns[index].name.humanize - expect(column.data).to eq 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' } + 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 } + allow(Post).to receive(:human_attribute_name).with(:title) { localized_name } end - it 'gets name from I18n' do - title_index = Post.content_columns.map(&:name).index('title') + 1 # First col is always id + 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.tap(&:exec_columns) } + context "when empty" do + let(:builder) { ActiveAdmin::CSVBuilder.new.tap(&:exec_columns) } it "should have no columns" do expect(builder.columns).to eq [] @@ -58,7 +67,7 @@ end describe "the column" do - let(:column){ builder.columns.first } + let(:column) { builder.columns.first } it "should have a name of 'Title'" do expect(column.name).to eq "Title" @@ -84,7 +93,7 @@ end describe "the column" do - let(:column){ builder.columns.first } + let(:column) { builder.columns.first } it "should have a name of 'My title'" do expect(column.name).to eq "My title" @@ -105,7 +114,7 @@ end describe "the column" do - let(:column){ builder.columns.first } + let(:column) { builder.columns.first } it "should have a name of 'my_title'" do expect(column.name).to eq "my_title" @@ -121,7 +130,7 @@ end describe "the column" do - let(:column){ builder.columns.first } + let(:column) { builder.columns.first } it "should have a name of 'my_title'" do expect(column.name).to eq "my_title" @@ -136,7 +145,7 @@ end it "should have proper separator" do - expect(builder.options).to eq({col_sep: ";"}) + expect(builder.options).to include(col_sep: ";") end end @@ -148,7 +157,7 @@ end describe "the column" do - let(:column){ builder.columns.first } + let(:column) { builder.columns.first } it "should have humanize_name option set" do expect(column.options).to eq humanize_name: false @@ -166,20 +175,20 @@ end it "should have proper separator" do - expect(builder.options).to eq({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(: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.tap { |b| b.exec_columns(dummy_view_context) } end it "should build columns provided by the controller" do @@ -189,17 +198,13 @@ context "build csv using the supplied order" do before do - @post1 = Post.create!(title: "Hello1", published_at: Date.today - 2.day ) - @post2 = Post.create!(title: "Hello2", published_at: Date.today - 1.day ) + @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) { + let(:dummy_controller) do class DummyController - def find_collection(*) - collection - end - - def collection - Post.order('published_at DESC') + def in_paginated_batches(&block) + Post.order("published_date DESC").each(&block) end def apply_decorator(resource) @@ -210,12 +215,12 @@ def view_context end end DummyController.new - } + end let(:builder) do ActiveAdmin::CSVBuilder.new do column "id" column "title" - column "published_at" + column "published_date" end end @@ -224,30 +229,13 @@ def view_context expect(builder).to receive(:build_row).and_return([]).once.ordered { |post| expect(post.id).to eq @post1.id } builder.build dummy_controller, [] end - - it "should generate data ignoring pagination" do - expect(dummy_controller).to receive(:find_collection). - with(except: :pagination).once. - and_call_original - expect(builder).to receive(:build_row).and_return([]).twice - builder.build dummy_controller, [] - end - end context "build csv using specified encoding and encoding_options" do let(:dummy_controller) do class DummyController - def find_collection(*) - collection - end - - def collection - Post - end - - def apply_decorator(resource) - resource + def in_paginated_batches(&block) + Post.all.each(&block) end def view_context @@ -255,8 +243,6 @@ def view_context end DummyController.new end - let(:encoding) { Encoding::ASCII } - let(:opts) { {} } let(:builder) do ActiveAdmin::CSVBuilder.new(encoding: encoding, encoding_options: opts) do column "おはようございます" @@ -292,12 +278,51 @@ def view_context end end - skip '#exec_columns' + context 'csv injection' do + let(:dummy_controller) do + class DummyController + def in_paginated_batches(&block) + Post.all.each(&block) + end - skip '#build_row' do - it 'renders non-strings' - it 'encodes values correctly' - it 'passes custom encoding options to String#encode!' - 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 index c0a798ebb84..80f5dada301 100644 --- a/spec/unit/dependency_spec.rb +++ b/spec/unit/dependency_spec.rb @@ -1,135 +1,143 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Dependency do - - k = ActiveAdmin::Dependency - - describe 'method_missing' do +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') + .and_return "foo" => Gem::Specification.new("foo", "1.2.3") end - it 'returns a Matcher' do - expect(k.foo).to be_a ActiveAdmin::Dependency::Matcher - expect(k.foo.inspect).to eq '' - expect(k.bar.inspect).to eq '' + 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(k.foo?).to eq true - expect(k.bar?).to eq false + describe "`?`" do + it "base" do + expect(described_class.foo?).to eq true + expect(described_class.bar?).to eq false end - it '=' do - expect(k.foo? '= 1.2.3').to eq true - expect(k.foo? '= 1' ).to eq false + + it "=" do + expect(described_class.foo? "= 1.2.3").to eq true + expect(described_class.foo? "= 1").to eq false end - it '>' do - expect(k.foo? '> 1').to eq true - expect(k.foo? '> 2').to eq false + + it ">" do + expect(described_class.foo? "> 1").to eq true + expect(described_class.foo? "> 2").to eq false end - it '<' do - expect(k.foo? '< 2').to eq true - expect(k.foo? '< 1').to eq false + + it "<" do + expect(described_class.foo? "< 2").to eq true + expect(described_class.foo? "< 1").to eq false end - it '>=' do - expect(k.foo? '>= 1.2.3').to eq true - expect(k.foo? '>= 1.2.2').to eq true - expect(k.foo? '>= 1.2.4').to eq false + + 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(k.foo? '<= 1.2.3').to eq true - expect(k.foo? '<= 1.2.4').to eq true - expect(k.foo? '<= 1.2.2').to eq false + + 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(k.foo? '~> 1.2.0').to eq true - expect(k.foo? '~> 1.1' ).to eq true - expect(k.foo? '~> 1.2.4').to eq false + + 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{ k.foo! '5' }.to raise_error ActiveAdmin::DependencyError, - 'You provided foo 1.2.3 but we need: 5.' + 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{ k.foo! '> 1', '< 1.2' }.to raise_error ActiveAdmin::DependencyError, - 'You provided foo 1.2.3 but we need: > 1, < 1.2.' + + 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{ k.bar! }.to raise_error ActiveAdmin::DependencyError, - 'To use bar you need to specify it in your Gemfile.' + + 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 + describe "[]" do before do allow(Gem).to receive(:loaded_specs) - .and_return 'a-b' => Gem::Specification.new('a-b', '1.2.3') + .and_return "a-b" => Gem::Specification.new("a-b", "1.2.3") end - it 'allows access to gems with an arbitrary name' do - expect(k['a-b']).to be_a ActiveAdmin::Dependency::Matcher - expect(k['a-b'].inspect).to eq '' - expect(k['c-d'].inspect).to eq '' + 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(k['a-b'].match? ).to eq true - expect(k['a-b'].match? '1.2.3').to eq true - expect(k['b-c'].match? ).to eq false + 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(k['a-b'].match! ).to eq nil - expect(k['a-b'].match! '1.2.3').to eq nil + 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{ k['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["a-b"].match! "2.5" } + .to raise_error(ActiveAdmin::DependencyError, "You provided a-b 1.2.3 but we need: 2.5.") - expect{ k['b-c'].match! }.to raise_error ActiveAdmin::DependencyError, - 'To use b-c you need to specify it in your Gemfile.' + 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(k['a-b'] == '1.2.3').to eq true - expect(k['a-b'] == '1.2' ).to eq false - expect(k['a-b'] == 1 ).to eq false + 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(k['a-b'] > 1).to eq true - expect(k['a-b'] > 2).to eq false + + it ">" do + expect(described_class["a-b"] > 1).to eq true + expect(described_class["a-b"] > 2).to eq false end - it '<' do - expect(k['a-b'] < 2).to eq true - expect(k['a-b'] < 1).to eq false + + it "<" do + expect(described_class["a-b"] < 2).to eq true + expect(described_class["a-b"] < 1).to eq false end - it '>=' do - expect(k['a-b'] >= '1.2.3').to eq true - expect(k['a-b'] >= '1.2.2').to eq true - expect(k['a-b'] >= '1.2.4').to eq false + + 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(k['a-b'] <= '1.2.3').to eq true - expect(k['a-b'] <= '1.2.4').to eq true - expect(k['a-b'] <= '1.2.2').to eq false + + 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{ k['b-c'] < 23 }.to raise_error ActiveAdmin::DependencyError, - 'To use b-c you need to specify it in your Gemfile.' + 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 9c8bff9da4f..4f6575543d0 100644 --- a/spec/unit/devise_spec.rb +++ b/spec/unit/devise_spec.rb @@ -1,7 +1,7 @@ -require 'rails_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,10 +12,21 @@ def self.helper(*); end end let(:controller) { controller_class.new } + let(:action_controller_config) { Rails.configuration.action_controller } - context 'with a RAILS_RELATIVE_URL_ROOT set' do + 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 - before { Rails.configuration.action_controller[:relative_url_root] = '/foo' } + yield + ensure + action_controller_config[:relative_url_root] = previous_relative_url_root + end + + 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 expect(controller.root_path).to eq "/foo/admin" @@ -25,12 +36,12 @@ def self.helper(*); end 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 expect(controller.root_path).to eq "/admin" @@ -40,12 +51,10 @@ def self.helper(*); end 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 @@ -69,28 +78,5 @@ def self.helper(*); end it "should include scope path in root_path" do expect(controller.root_path).to eq "#{SCOPE}/admin" end - end - - describe "#config" do - let(:config) { ActiveAdmin::Devise.config } - - describe ":sign_out_via option" do - it "should contain the application.logout_link_method" do - expect(::Devise).to receive(:sign_out_via).and_return(:delete) - expect(ActiveAdmin.application).to receive(:logout_link_method).and_return(:get) - - expect(config[:sign_out_via]).to include(:get) - end - - it "should contain Devise's logout_via_method(s)" do - expect(::Devise).to receive(:sign_out_via).and_return([:delete, :post]) - expect(ActiveAdmin.application).to receive(:logout_link_method).and_return(:get) - - expect(config[:sign_out_via]).to eq [:delete, :post, :get] - 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 5eff8aa22ef..869b05ac9ac 100644 --- a/spec/unit/dsl_spec.rb +++ b/spec/unit/dsl_spec.rb @@ -1,31 +1,27 @@ -require 'rails_helper' - +# frozen_string_literal: true +require "rails_helper" module MockModuleToInclude def self.included(dsl) end end -describe ActiveAdmin::DSL do - +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(resource_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 expect(MockModuleToInclude).to receive(:included).with(dsl) dsl.run_registration_block do include MockModuleToInclude end end - end - - describe '#action_item' do + describe "#action_item" do before do @default_items_count = resource_config.action_items.size @@ -39,37 +35,18 @@ def self.included(dsl) it "adds action_item to the action_items of config" do expect(resource_config.action_items.size).to eq(@default_items_count + 1) end - - context 'DEPRECATED: when used without a name' do - it "is configured for only the show action" do - expect(ActiveAdmin::Deprecation).to receive(:warn).with(instance_of(String)) - - dsl.run_registration_block do - action_item only: :edit do - "Awesome ActionItem" - end - end - - item = resource_config.action_items.last - expect(item.display_on?(:edit)).to be true - expect(item.display_on?(:index)).to be false - end - end end describe "#menu" do - it "should set the menu_item_options on the configuration" do - expect(resource_config).to 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" end end - end describe "#navigation_menu" do - it "should set the navigation_menu_name on the configuration" do expect(resource_config).to receive(:navigation_menu_name=).with(:admin) dsl.run_registration_block do @@ -84,11 +61,9 @@ def self.included(dsl) end expect(resource_config.navigation_menu_name).to eq :dynamic_menu end - end describe "#sidebar" do - before do dsl.config.sidebar_sections << ActiveAdmin::SidebarSection.new(:email) end @@ -97,9 +72,8 @@ def self.included(dsl) dsl.run_registration_block do sidebar :help end - expect(dsl.config.sidebar_sections.map(&:name)).to match_array %w{filters search_status email help} + expect(dsl.config.sidebar_sections.map(&:name)).to match_array ["filters", "active_search", "email", "help"] end - end describe "#batch_action" do 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/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 5a7cafeb519..00000000000 --- a/spec/unit/filters/filter_form_builder_spec.rb +++ /dev/null @@ -1,433 +0,0 @@ -require 'rails_helper' - -class Post - ransacker :custom_searcher do - # nothing to see here - end -end - -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.to_s - end - - def filter(name, options = {}) - render_filter scope, name => options - end - - let(:scope) { Post.search } - - describe "the form in general" do - let(:body) { Capybara.string(filter :title) } - - it "should generate a form which submits via get" do - expect(body).to have_selector("form.filter_form[method=get]") - end - - it "should generate a filter button" do - expect(body).to have_selector("input[type=submit][value=Filter]") - end - - it "should only generate the form once" do - expect(body).to have_selector("form", count: 1) - end - - it "should generate a clear filters link" do - expect(body).to have_selector("a.clear_filters_btn", text: "Clear Filters") - end - - describe "label as proc" do - let(:body) { Capybara.string(filter :title, label: proc { 'Title from proc' }) } - - it "should render proper label" do - expect(body).to have_selector("label", text: "Title from proc") - end - end - end - - describe "string attribute" do - let(:body) { Capybara.string(filter :title) } - - it "should generate a select option for starts with" do - expect(body).to have_selector("option[value=title_starts_with]", text: "Starts with") - end - - it "should generate a select option for ends with" do - expect(body).to have_selector("option[value=title_ends_with]", text: "Ends with") - end - - it "should generate a select option for contains" do - expect(body).to have_selector("option[value=title_contains]", text: "Contains") - end - - it "should generate a text field for input" do - expect(body).to have_selector("input[name='q[title_contains]']") - end - - it "should have a proper label" do - expect(body).to have_selector("label", text: "Title") - end - - it "should translate the label for text field" do - with_translation activerecord: {attributes: {post: {title: 'Name'}}} do - expect(body).to have_selector("label", text: "Name") - end - end - - it "should select the option which is currently being filtered" do - scope = Post.search title_starts_with: "foo" - body = Capybara.string(render_filter scope, title: {}) - expect(body).to have_selector("option[value=title_starts_with][selected=selected]", text: "Starts with") - end - - context "with predicate" do - %w[eq equals cont contains start starts_with end ends_with].each do |predicate| - describe "'#{predicate}'" do - let(:body) { Capybara.string(filter :"title_#{predicate}") } - - it "shouldn't include a select field" do - expect(body).not_to have_selector("select") - end - - it "should build correctly" do - expect(body).to have_selector("input[name='q[title_#{predicate}]']") - end - end - end - end - end - - describe "text attribute" do - let(:body) { Capybara.string(filter :body) } - - it "should generate a search field for a text attribute" do - expect(body).to have_selector("input[name='q[body_contains]']") - end - - it "should have a proper label" do - expect(body).to have_selector("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 uniq: double(pluck: ['A Title']) - expect(m.uniq).to receive(:pluck).with :title - m - } - body - end - end - end - - describe "datetime attribute" do - let(:body) { Capybara.string(filter :created_at) } - - it "should generate a date greater than" do - expect(body).to have_selector("input.datepicker[name='q[created_at_gteq]']") - end - it "should generate a seperator" do - expect(body).to have_selector("span.seperator") - end - it "should generate a date less than" do - expect(body).to have_selector("input.datepicker[name='q[created_at_lteq]']") - end - end - - describe "integer attribute" do - let(:body) { Capybara.string(filter :id) } - - it "should generate a select option for equal to" do - expect(body).to have_selector("option[value=id_equals]", text: "Equals") - end - it "should generate a select option for greater than" do - expect(body).to have_selector("option", text: "Greater than") - end - it "should generate a select option for less than" do - expect(body).to have_selector("option", text: "Less than") - end - it "should generate a text field for input" do - expect(body).to have_selector("input[name='q[id_equals]']") - end - it "should select the option which is currently being filtered" do - scope = Post.search id_greater_than: 1 - body = Capybara.string(render_filter scope, id: {}) - expect(body).to have_selector("option[value=id_greater_than][selected=selected]", text: "Greater than") - end - end - - describe "boolean attribute" do - context "boolean datatypes" do - let(:body) { Capybara.string(filter :starred) } - - it "should generate a select" do - expect(body).to have_selector("select[name='q[starred_eq]']") - end - it "should set the default text to 'Any'" do - expect(body).to have_selector("option[value='']", text: "Any") - end - it "should create an option for true and false" do - expect(body).to have_selector("option[value=true]", text: "Yes") - expect(body).to have_selector("option[value=false]", text: "No") - end - - it "should translate the label for boolean field" do - with_translation activerecord: {attributes: {post: {starred: 'Faved'}}} do - expect(body).to have_selector("label", text: "Faved") - end - end - end - - context "non-boolean data types" do - let(:body) { Capybara.string(filter :title_present, as: :boolean) } - - it "should generate a select" do - expect(body).to have_selector("select[name='q[title_present]']") - end - it "should set the default text to 'Any'" do - expect(body).to have_selector("option[value='']", text: "Any") - end - it "should create an option for true and false" do - expect(body).to have_selector("option[value=true]", text: "Yes") - expect(body).to have_selector("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) { Capybara.string(filter :author_id) } - - it "should generate a numeric filter" do - expect(body).to have_selector("label", text: "Author") # really this should be Author ID :/) - expect(body).to have_selector("option[value=author_id_less_than]") - expect(body).to have_selector("input#q_author_id[name='q[author_id_equals]']") - end - end - - context "when given as the name of the relationship" do - let(:body) { Capybara.string(filter :author) } - - it "should generate a select" do - expect(body).to have_selector("select[name='q[author_id_eq]']") - end - it "should set the default text to 'Any'" do - expect(body).to have_selector("option[value='']", text: "Any") - end - it "should create an option for each related object" do - expect(body).to have_selector("option[value='#{@john.id}']", text: "John Doe") - expect(body).to have_selector("option[value='#{@jane.id}']", text: "Jane Doe") - end - - context "with a proc" do - let :body do - Capybara.string(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_selector("option", text: "Title One") - expect(body).to have_selector("option", text: "Title Two") - end - - it "should render the collection in the context of the view" do - body = Capybara.string(filter(:title, as: :select, collection: proc{[a_helper_method]})) - expect(body).to have_selector("option", text: "A Helper Method") - end - end - end - - context "as check boxes" do - let(:body) { Capybara.string(filter :author, as: :check_boxes) } - - it "should create a check box for each related object" do - expect(body).to have_selector("input[type=checkbox][name='q[author_id_in][]'][value='#{@jane.id}']") - expect(body).to have_selector("input[type=checkbox][name='q[author_id_in][]'][value='#{@jane.id}']") - end - end - - context "when polymorphic relationship" do - let(:scope) { ActiveAdmin::Comment.search } - 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.search } - let(:body) { Capybara.string(filter :category) } - it "should ignore that foreign key and let Ransack handle it" do - expect(Post.reflect_on_association(:category).foreign_key).to eq :custom_category_id - expect(body).to have_selector("select[name='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 - # Setup an ActionView::Base object which can be used for - # generating the form for. - let(:helpers) do - view = action_view - def view.collection_path - "/categories" - end - - def view.protect_against_forgery? - false - end - - def view.a_helper_method - "A Helper Method" - end - - view - end - let(:scope) { Category.search } - - 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) { Capybara.string(filter :authors) } - - it "should generate a select" do - expect(body).to have_selector("select[name='q[posts_author_id_eq]']") - end - - it "should set the default text to 'Any'" do - expect(body).to have_selector("option[value='']", text: "Any") - end - - it "should create an option for each related object" do - expect(body).to have_selector("option[value='#{john.id}']", text: "John Doe") - expect(body).to have_selector("option[value='#{jane.id}']", text: "Jane Doe") - end - end - - context "as check boxes" do - let(:body) { Capybara.string(filter :authors, as: :check_boxes) } - - it "should create a check box for each related object" do - expect(body).to have_selector("input[name='q[posts_author_id_in][]'][type=checkbox][value='#{john.id}']") - expect(body).to have_selector("input[name='q[posts_author_id_in][]'][type=checkbox][value='#{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 = Capybara.string(filter :body, verb => proc{ true }) - expect(body).send if_true, have_selector("input[name='q[body_contains]']") - end - it "#{should} be displayed if false" do - body = Capybara.string(filter :body, verb => proc{ false }) - expect(body).send if_false, have_selector("input[name='q[body_contains]']") - 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).not_to have_selector("input[name='q[body_contains]']") - 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).not_to have_selector("input[name='q[body_contains]']") - expect(body).to have_selector("select[name='q[author_id_eq]']") - end - end - end - end - - describe "custom search methods" do - - it "should work as select" do - body = Capybara.string(filter :custom_searcher, as: :select, collection: ['foo']) - expect(body).to have_selector("select[name='q[custom_searcher]']") - end - - it "should work as string" do - body = Capybara.string(filter :custom_searcher, as: :string) - expect(body).to have_selector("input[name='q[custom_searcher]']") - end - end - - describe "does not support some filter inputs" do - it "should fallback to use formtastic inputs" do - body = Capybara.string(filter :custom_searcher, as: :text) - expect(body).to have_selector("textarea[name='q[custom_searcher]']") - end - end - - describe "blank option" do - context "for a select filter" do - it "should be there by default" do - body = Capybara.string(filter(:author)) - expect(body).to have_selector("option", text: "Any") - end - it "should be able to be disabled" do - body = Capybara.string(filter(:author, include_blank: false)) - expect(body).to_not have_selector("option", text: "Any") - end - end - - context "for a multi-select filter" do - it "should not be there by default" do - body = Capybara.string(filter(:author, multiple: true)) - expect(body).to_not have_selector("option", text: "Any") - end - it "should be able to be enabled" do - body = Capybara.string(filter(:author, multiple: true, include_blank: true)) - expect(body).to have_selector("option", text: "Any") - end - end - end - -end diff --git a/spec/unit/filters/humanized_spec.rb b/spec/unit/filters/humanized_spec.rb deleted file mode 100644 index 5bf845e0aec..00000000000 --- a/spec/unit/filters/humanized_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::Filters::Humanized do - describe '#value' do - it 'should equal query string parameter if not an Array' do - param = ['category_id_eq', '1'] - subject = ActiveAdmin::Filters::Humanized.new(param) - expect(subject.value).to eq('1') - end - - it 'should equal query string parameters separated by commas if an Array' do - param = ['category_id_eq', ['1', '2']] - subject = ActiveAdmin::Filters::Humanized.new(param) - expect(subject.value).to eq("1, 2") - end - - it 'should remove nil values before joining equal query string parameters separated by commas if an Array' do - param = ['category_id_eq', ['1', nil, '2']] - subject = ActiveAdmin::Filters::Humanized.new(param) - expect(subject.value).to eq("1, 2") - end - end - - describe '#body' do - context 'when Ransack predicate' do - it 'parses language from Ransack' do - param = ['category_id_eq', '1'] - subject = ActiveAdmin::Filters::Humanized.new(param) - expect(subject.body).to eq('Category ID equals') - end - - it 'handles strings with embedded predicates' do - param = ['requires_approval_eq', '1'] - humanizer = ActiveAdmin::Filters::Humanized.new(param) - expect(humanizer.value).to eq('1') - expect(humanizer.body).to eq('Requires Approval equals') - end - end - - context 'when ActiveAdmin predicate' do - it 'parses language from ActiveAdmin' do - param = ['name_starts_with', 'test'] - humanizer = ActiveAdmin::Filters::Humanized.new(param) - expect(humanizer.body).to eq('Name starts with') - end - end - - context 'when unknown predicate' do - it 'uses raw predicate string' do - param = ['name_predicate_does_not_exist', 'test'] - humanizer = ActiveAdmin::Filters::Humanized.new(param) - expect(humanizer.body).to eq("Name Predicate Does Not Exist") - end - end - end -end diff --git a/spec/unit/filters/resource_spec.rb b/spec/unit/filters/resource_spec.rb index cd3819f1bde..dc253d39e64 100644 --- a/spec/unit/filters/resource_spec.rb +++ b/spec/unit/filters/resource_spec.rb @@ -1,7 +1,7 @@ -require 'rails_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) @@ -12,9 +12,11 @@ end it "should return the defaults if no filters are set" do - expect(resource.filters.keys).to match_array([ - :author, :body, :category, :created_at, :custom_searcher, :position, :published_at, :starred, :taggings, :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 @@ -34,9 +36,11 @@ 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_searcher, :position, :published_at, :starred, :title, :updated_at - ]) + 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 @@ -48,7 +52,7 @@ it "should work as a string" do expect(resource.filters.keys).to include :author - resource.remove_filter 'author' + resource.remove_filter "author" expect(resource.filters.keys).to_not include :author end @@ -64,7 +68,7 @@ it "should raise an exception when filters are disabled" do resource.filters = false - expect{ resource.remove_filter :author }.to raise_error ActiveAdmin::Filters::Disabled + expect { resource.remove_filter :author }.to raise_error(ActiveAdmin::Filters::Disabled, /Cannot remove a filter/) end end @@ -83,34 +87,36 @@ end it "should work as a string" do - resource.add_filter 'title' + resource.add_filter "title" expect(resource.filters).to eq title: {} end it "should work with specified options" do resource.add_filter :title, as: :string - expect(resource.filters).to eq 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} + 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 - expect(resource.filters.keys).to match_array([ - :author, :body, :category, :count, :created_at, :custom_searcher, :position, :published_at, :starred, :taggings, :title, :updated_at - ]) + 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 }.to raise_error ActiveAdmin::Filters::Disabled + expect { resource.add_filter :title }.to raise_error(ActiveAdmin::Filters::Disabled, /Cannot add a filter/) end end @@ -124,5 +130,4 @@ it "should add a sidebar section for the filters" do 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 9640b4dcbbb..a4d6201b9de 100644 --- a/spec/unit/form_builder_spec.rb +++ b/spec/unit/form_builder_spec.rb @@ -1,12 +1,12 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" require "rspec/mocks/standalone" -describe ActiveAdmin::FormBuilder do - +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 @@ -16,36 +16,35 @@ 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" - end - - def view.fa_icon(*args) - args.inspect + 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) - form = render_arbre_component({form_object: form_object, form_options: options, form_block: block}, helpers) do + 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 "it without custom settings" do + context "without custom settings" do let :body do build_form do |f| f.inputs do @@ -56,14 +55,14 @@ def build_form(options = {}, form_object = Post.new, &block) end it "should generate a fieldset with a inputs class" do - expect(body).to have_selector("fieldset.inputs") + expect(body).to have_css("fieldset.inputs") end end - context "it with custom settings" do + context "with custom settings" do let :body do build_form do |f| - f.inputs class: "custom_class" do + f.inputs class: "custom_class", name: "custom_name", custom_attr: "custom_attr", data: { test: "custom" } do f.input :title f.input :body end @@ -71,7 +70,34 @@ def build_form(options = {}, form_object = Post.new, &block) end it "should generate a fieldset with a inputs and custom class" do - expect(body).to have_selector("fieldset.inputs.custom_class") + 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 @@ -90,29 +116,32 @@ def build_form(options = {}, form_object = Post.new, &block) end end - it "should generate a text input" do - expect(body).to have_selector("input[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 - expect(body).to have_selector("textarea[name='post[body]']") + expect(body).to have_css("textarea[name='post[body]']") end + it "should only generate the form once" do - expect(body).to have_selector("form", count: 1) + expect(body).to have_css("form", count: 1) end + it "should generate actions" do - expect(body).to have_selector("input[type=submit][value='Submit Me']") - expect(body).to have_selector("input[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 - expect { + 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 - }.to raise_error(Formtastic::PolymorphicInputWithoutCollectionError) + end.to raise_error(Formtastic::PolymorphicInputWithoutCollectionError) end end @@ -124,21 +153,19 @@ def build_form(options = {}, form_object = Post.new, &block) end end it "should pass the options on to the form" do - expect(body).to have_selector("form[enctype='multipart/form-data']") + expect(body).to have_css("form[enctype='multipart/form-data']") end end - if Rails::VERSION::MAJOR > 3 - context "file input present" do - let :body do - build_form do |f| - f.input :body, as: :file - end + context "file input present" do + let :body do + build_form do |f| + f.input :body, as: :file end + end - it "adds multipart attribute automatically" do - expect(body).to have_selector("form[enctype='multipart/form-data']") - end + it "adds multipart attribute automatically" do + expect(body).to have_css("form[enctype='multipart/form-data']") end end @@ -150,15 +177,41 @@ def build_form(options = {}, form_object = Post.new, &block) end f.actions end - expect(body).to have_selector("[id=post_title]", count: 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 - expect(body).to have_selector("[type=submit]", count: 1) - expect(body).to have_selector("[class=cancel]", count: 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 @@ -166,50 +219,50 @@ def build_form(options = {}, form_object = Post.new, &block) f.action :submit, label: "Create & Edit" end end - expect(body).to have_selector("[type=submit]", count: 2) - expect(body).to have_selector("[class=cancel]", count: 0) + expect(body).to have_css("[type=submit]", count: 2) + expect(body).to have_css(".cancel", count: 0) end - end context "with Arbre inside" do it "should render the Arbre in the expected place" do body = build_form do |f| div do - h1 'Heading' + h1 "Heading" end f.inputs do - span 'Top note' + span "Top note" f.input :title - span 'Bottom note' + span "Bottom note" end - h3 'Footer' + h3 "Footer" f.actions end - expect(body).to have_selector("div > h1") - expect(body).to have_selector("h1", count: 1) - expect(body).to have_selector(".inputs > ol > span") - expect(body).to have_selector("span", count: 2) + 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 allow a simplified syntax" do body = build_form do |f| div do - h1 'Heading' + h1 "Heading" end inputs do - span 'Top note' + span "Top note" input :title - span 'Bottom note' + span "Bottom note" end - h3 'Footer' + h3 "Footer" actions end - expect(body).to have_selector("div > h1") - expect(body).to have_selector("h1", count: 1) - expect(body).to have_selector(".inputs > ol > span") - expect(body).to have_selector("span", count: 2) + 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 @@ -220,10 +273,11 @@ def build_form(options = {}, form_object = Post.new, &block) end end it "should have a title input" do - expect(body).to have_selector("input[type=text][name='post[title]']") + expect(body).to have_field("post[title]", type: "text") end + it "should have a body textarea" do - expect(body).to have_selector("textarea[name='post[body]']") + expect(body).to have_css("textarea[name='post[body]']") end end @@ -243,7 +297,7 @@ def build_form(options = {}, form_object = Post.new, &block) end end it "should generate a nested text input once" do - expect(body).to have_selector("[id=post_author_attributes_first_name_input]", count: 1) + expect(body).to have_css("[id=post_author_attributes_first_name_input]", count: 1) end end @@ -260,7 +314,7 @@ def build_form(options = {}, form_object = Post.new, &block) end end it "should create 2 options" do - expect(body).to have_selector("option", count: 2) + expect(body).to have_css("option", count: 2) end end @@ -271,10 +325,38 @@ def build_form(options = {}, form_object = Post.new, &block) end end it "should create 2 radio buttons" do - expect(body).to have_selector("[type=radio]", count: 2) + expect(body).to have_css("[type=radio]", count: 2) + end + end + end + + context "with inputs component inside has_many" do + def user + u = User.new + u.profile = Profile.new(bio: "bio") + u + end + + let :body do + author = user() + build_form do |f| + f.form_builder.instance_eval do + @object.author = author + end + f.inputs name: "Author", for: :author do |author| + author.has_many :profile, allow_destroy: true do |profile| + profile.inputs "inputs for profile #{profile.object.bio}" do + profile.input :bio + end + end + end end end + it "should see the profile fields for an existing profile" do + expect(body).to have_css("[id='post_author_attributes_profile_attributes_bio']", count: 1) + expect(body).to have_css("textarea[name='post[author_attributes][profile_attributes][bio]']") + end end context "with a has_one relation on an author's profile" do @@ -288,7 +370,7 @@ def build_form(options = {}, form_object = Post.new, &block) f.form_builder.instance_eval do @object.author = author end - f.inputs name: 'Author', for: :author do |author| + f.inputs name: "Author", for: :author do |author| author.has_many :profile, allow_destroy: true do |profile| profile.input :bio end @@ -300,7 +382,7 @@ def build_form(options = {}, form_object = Post.new, &block) def user User.new end - expect(body).to have_selector("a[contains(data-html,'post[author_attributes][profile_attributes][bio]')]") + expect(body).to have_css("a[contains(data-html,'post[author_attributes][profile_attributes][bio]')]") end it "should see the profile fields for an existing profile" do @@ -309,19 +391,20 @@ def user u.profile = Profile.new u end - expect(body).to have_selector("[id='post_author_attributes_profile_attributes_bio']", count: 1) - expect(body).to have_selector("textarea[name='post[author_attributes][profile_attributes][bio]']") + expect(body).to have_css("[id='post_author_attributes_profile_attributes_bio']", count: 1) + expect(body).to have_css("textarea[name='post[author_attributes][profile_attributes][bio]']") end end shared_examples :inputs_with_for_expectation do it "should generate a nested text input once" do - expect(body).to have_selector("[id=post_author_attributes_first_name_input]", count: 1) - expect(body).to have_selector("[id=post_author_attributes_last_name_input]", count: 1) + expect(body).to have_css("[id=post_author_attributes_first_name_input]", count: 1) + expect(body).to have_css("[id=post_author_attributes_last_name_input]", count: 1) end + it "should add author first and last name fields" do - expect(body).to have_selector("input[name='post[author_attributes][first_name]']") - expect(body).to have_selector("input[name='post[author_attributes][last_name]']") + expect(body).to have_field("post[author_attributes][first_name]") + expect(body).to have_field("post[author_attributes][last_name]") end end @@ -335,7 +418,7 @@ def user f.form_builder.instance_eval do @object.author = User.new end - f.inputs name: 'Author', for: :author do |author| + f.inputs name: "Author", for: :author do |author| author.inputs :first_name, :last_name end end @@ -354,7 +437,7 @@ def user f.form_builder.instance_eval do @object.author = User.new end - f.inputs name: 'Author', for: :author do |author| + f.inputs name: "Author", for: :author do |author| author.input :first_name author.input :last_name end @@ -370,7 +453,7 @@ def user f.form_builder.instance_eval do @object.author = User.new end - f.inputs name: 'Author', for: :author do |author| + f.inputs name: "Author", for: :author do |author| author.input :first_name author.input :last_name end @@ -389,7 +472,7 @@ def user body = build_form do |f| f.input :title, wrapper_html: { class: "important" } end - expect(body).to have_selector("li[class='important string input optional stringish']") + expect(body).to have_css("li[class='important string input optional stringish']") end end @@ -402,25 +485,25 @@ def user end f.inputs do f.input :author - f.input :published_at + f.input :created_at end end end it "should render four inputs" do - expect(body).to have_selector("input[name='post[title]']", count: 1) - expect(body).to have_selector("textarea[name='post[body]']", count: 1) - expect(body).to have_selector("select[name='post[author_id]']", count: 1) - expect(body).to have_selector("select[name='post[published_at(1i)]']", count: 1) - expect(body).to have_selector("select[name='post[published_at(2i)]']", count: 1) - expect(body).to have_selector("select[name='post[published_at(3i)]']", count: 1) - expect(body).to have_selector("select[name='post[published_at(4i)]']", count: 1) + expect(body).to have_field("post[title]", count: 1) + expect(body).to have_css("textarea[name='post[body]']", count: 1) + expect(body).to have_select("post[author_id]", count: 1) + expect(body).to have_select("post[created_at(1i)]", count: 1) + expect(body).to have_select("post[created_at(2i)]", count: 1) + expect(body).to have_select("post[created_at(3i)]", count: 1) + expect(body).to have_select("post[created_at(4i)]", count: 1) end end context "with has many inputs" do describe "with simple block" do let :body do - build_form({url: '/categories'}, Category.new) do |f| + build_form({ url: "/categories" }, Category.new) do |f| f.object.posts.build f.has_many :posts do |p| p.input :title @@ -433,53 +516,57 @@ def user let(:valid_html_id) { /^[A-Za-z]+[\w\-\:\.]*$/ } it "should translate the association name in header" do - with_translation activerecord: {models: {post: {one: 'Blog Post', other: 'Blog Posts'}}} do - expect(body).to have_selector("h3", text: "Blog Posts") + with_translation %i[activerecord models post one], "Blog Post" do + with_translation %i[activerecord models post other], "Blog Posts" do + expect(body).to have_css("h3", text: "Blog Posts") + end end end it "should use model name when there is no translation for given model in header" do - expect(body).to have_selector("h3", text: "Post") + expect(body).to have_css("h3", text: "Post") end it "should translate the association name in has many new button" do - with_translation activerecord: {models: {post: {one: 'Blog Post', other: 'Blog Posts'}}} do - expect(body).to have_selector("a", text: "Add New Blog Post") + with_translation %i[activerecord models post one], "Blog Post" do + with_translation %i[activerecord models post other], "Blog Posts" do + expect(body).to have_css("a", text: "Add New Blog Post") + end end end it "should translate the attribute name" do - with_translation activerecord: {attributes: {post: {title: 'A very nice title'}}} do - expect(body).to have_selector("label", text: "A very nice title") + with_translation %i[activerecord attributes post title], "A very nice title" do + expect(body).to have_css("label", text: "A very nice title") end end it "should use model name when there is no translation for given model in has many new button" do - expect(body).to have_selector("a", text: "Add New Post") + expect(body).to have_css("a", text: "Add New Post") end it "should render the nested form" do - expect(body).to have_selector("input[name='category[posts_attributes][0][title]']") - expect(body).to have_selector("textarea[name='category[posts_attributes][0][body]']") + expect(body).to have_field("category[posts_attributes][0][title]") + expect(body).to have_css("textarea[name='category[posts_attributes][0][body]']") end it "should add a link to remove new nested records" do - expect(body).to have_selector(".has_many_container > fieldset > ol > li > a.button.has_many_remove[href='#']", text: "Remove") + expect(body).to have_css(".has-many-container > fieldset > ol > li > a.has-many-remove[href='#']", text: "Remove") end it "should add a link to add new nested records" do - expect(body).to have_selector(".has_many_container > a.button.has_many_add[href='#']", text: "Add New Post") + expect(body).to have_css(".has-many-container > a.has-many-add[href='#']", text: "Add New Post") end it "should set an HTML-id valid placeholder" do - link = body.find('.has_many_container > a.button.has_many_add') + link = body.find(".has-many-container > a.has-many-add") expect(link[:'data-placeholder']).to match valid_html_id end describe "with namespaced model" do it "should set an HTML-id valid placeholder" do allow(Post).to receive(:name).and_return "ActiveAdmin::Post" - link = body.find('.has_many_container > a.button.has_many_add') + link = body.find(".has-many-container > a.has-many-add") expect(link[:'data-placeholder']).to match valid_html_id end end @@ -487,27 +574,26 @@ def user describe "with complex block" do let :body do - build_form({url: '/categories'}, Category.new) do |f| + build_form({ url: "/categories" }, Category.new) do |f| f.object.posts.build - f.has_many :posts do |p,i| + f.has_many :posts do |p, i| p.input :title, label: "Title #{i}" end end end it "should accept a block with a second argument" do - expect(body).to have_selector("label", text: "Title 1") + expect(body).to have_css("label", text: "Title 1") end it "should add a custom header" do - expect(body).to have_selector("h3", text: "Post") + expect(body).to have_css("h3", text: "Post") end - end describe "without heading and new record link" do let :body do - build_form({url: '/categories'}, Category.new) do |f| + build_form({ url: "/categories" }, Category.new) do |f| f.object.posts.build f.has_many :posts, heading: false, new_record: false do |p| p.input :title @@ -516,21 +602,21 @@ def user end it "should not add a header" do - expect(body).not_to have_selector("h3", text: "Post") + expect(body).to have_no_css("h3", text: "Post") end it "should not add link to new nested records" do - expect(body).not_to have_selector("a", text: "Add New Post") + expect(body).to have_no_css("a", text: "Add New Post") end it "should render the nested form" do - expect(body).to have_selector("input[name='category[posts_attributes][0][title]']") + expect(body).to have_field("category[posts_attributes][0][title]") end end describe "with custom heading" do let :body do - build_form({url: '/categories'}, Category.new) do |f| + build_form({ url: "/categories" }, Category.new) do |f| f.object.posts.build f.has_many :posts, heading: "Test heading" do |p| p.input :title @@ -539,67 +625,225 @@ def user end it "should add a custom header" do - expect(body).to have_selector("h3", "Test heading") + expect(body).to have_css("h3", text: "Test heading") end - end describe "with custom new record link" do let :body do - build_form({url: '/categories'}, Category.new) do |f| + build_form({ url: "/categories" }, Category.new) do |f| f.object.posts.build - f.has_many :posts, new_record: 'My Custom New Post' do |p| + f.has_many :posts, new_record: "My Custom New Post" do |p| p.input :title end end end it "should add a custom new record link" do - expect(body).to have_selector("a", text: "My Custom New Post") + expect(body).to have_css("a", text: "My Custom New Post") + end + end + + describe "with custom class" do + let :body do + build_form({ url: "/categories" }, Category.new) do |f| + f.object.posts.build + f.has_many :posts, class: 'myclass' do |p| + p.input :title + end + end end + it "should generate a fieldset with the given class" do + expect(body).to have_css(".has-many-container > fieldset.myclass") + end + + it "should add the custom class on the fieldset generated by the new record link" do + link = body.find(".has-many-container > a.has-many-add") + new_record_html = Capybara.string(link[:'data-html']) + expect(new_record_html).to have_css("fieldset.myclass") + end end - describe "with allow destroy" do - context "with an existing post" do - let :body do - build_form({url: '/categories'}, Category.new) do |f| - allow(f.object.posts.build).to receive(:new_record?).and_return(false) - f.has_many :posts, allow_destroy: true do |p| - p.input :title - end + describe "with custom attributes" do + let :body do + build_form({ url: "/categories" }, Category.new) do |f| + f.object.posts.build + f.has_many :posts, attr: "value", data: { 'custom-attribute': "custom-value" } do |p| + p.input :title end end + end + + it "should generate a fieldset with the given custom attributes" do + expect(body).to have_css(".has-many-container > fieldset[attr='value'][data-custom-attribute='custom-value']") + end + + it "should add custom attributes on the fieldset generated by the new record link" do + link = body.find(".has-many-container > a.has-many-add") + new_record_html = Capybara.string(link[:'data-html']) + expect(new_record_html).to have_css("fieldset[attr='value'][data-custom-attribute='custom-value']") + end + end + + describe "with allow destroy" do + shared_examples_for "has many with allow_destroy = true" do |child_num| + it "should render the nested form" do + expect(body).to have_field("category[posts_attributes][#{child_num}][title]") + end it "should include a boolean field for _destroy" do - expect(body).to have_selector("input[name='category[posts_attributes][0][_destroy]']") + expect(body).to have_field("category[posts_attributes][#{child_num}][_destroy]") end it "should have a check box with 'Remove' as its label" do - expect(body).to have_selector("label[for=category_posts_attributes_0__destroy]", text: "Delete") + expect(body).to have_css("label[for=category_posts_attributes_#{child_num}__destroy]", text: "Delete") end - it "should wrap the destroy field in an li with class 'has_many_delete'" do - expect(body).to have_selector(".has_many_container > fieldset > ol > li.has_many_delete > input", count: 1) + it "should wrap the destroy field in an li with class 'has-many-delete'" do + expect(body).to have_css(".has-many-container > fieldset > ol > li.has-many-delete > input", count: 1, visible: :hidden) end end - context "with a new post" do + shared_examples_for "has many with allow_destroy = false" do |child_num| + it "should render the nested form" do + expect(body).to have_field("category[posts_attributes][#{child_num}][title]") + end + + it "should not have a boolean field for _destroy" do + expect(body).to have_no_field("category[posts_attributes][#{child_num}][_destroy]", visible: :all) + end + + it "should not have a check box with 'Remove' as its label" do + expect(body).to have_no_css("label[for=category_posts_attributes_#{child_num}__destroy]", text: "Remove") + end + end + + shared_examples_for "has many with allow_destroy as String, Symbol or Proc" do |allow_destroy_option| let :body do - build_form({url: '/categories'}, Category.new) do |f| - f.object.posts.build - f.has_many :posts, allow_destroy: true do |p| + s = self + build_form({ url: "/categories" }, Category.new) do |f| + s.instance_exec do + allow(f.object.posts.build).to receive(:foo?).and_return(true) + allow(f.object.posts.build).to receive(:foo?).and_return(false) + + f.object.posts.each do |post| + allow(post).to receive(:new_record?).and_return(false) + end + end + f.has_many :posts, allow_destroy: allow_destroy_option do |p| p.input :title end end end - it "should not have a boolean field for _destroy" do - expect(body).not_to have_selector("input[name='category[posts_attributes][0][_destroy]']") + context "for the child that responds with true" do + it_behaves_like "has many with allow_destroy = true", 0 end - it "should not have a check box with 'Remove' as its label" do - expect(body).not_to have_selector("label[for=category_posts_attributes_0__destroy]", text: "Remove") + context "for the child that responds with false" do + it_behaves_like "has many with allow_destroy = false", 1 + end + end + + context "with an existing post" do + context "with allow_destroy = true" do + let :body do + s = self + build_form({ url: "/categories" }, Category.new) do |f| + s.instance_exec do + allow(f.object.posts.build).to receive(:new_record?).and_return(false) + end + f.has_many :posts, allow_destroy: true do |p| + p.input :title + end + end + end + + it_behaves_like "has many with allow_destroy = true", 0 + end + + context "with allow_destroy = false" do + let :body do + s = self + build_form({ url: "/categories" }, Category.new) do |f| + s.instance_exec do + allow(f.object.posts.build).to receive(:new_record?).and_return(false) + end + f.has_many :posts, allow_destroy: false do |p| + p.input :title + end + end + end + + it_behaves_like "has many with allow_destroy = false", 0 + end + + context "with allow_destroy = nil" do + let :body do + s = self + build_form({ url: "/categories" }, Category.new) do |f| + s.instance_exec do + allow(f.object.posts.build).to receive(:new_record?).and_return(false) + end + f.has_many :posts, allow_destroy: nil do |p| + p.input :title + end + end + end + + it_behaves_like "has many with allow_destroy = false", 0 + end + + context "with allow_destroy as Symbol" do + it_behaves_like("has many with allow_destroy as String, Symbol or Proc", :foo?) + end + + context "with allow_destroy as String" do + it_behaves_like("has many with allow_destroy as String, Symbol or Proc", "foo?") + end + + context "with allow_destroy as proc" do + it_behaves_like( + "has many with allow_destroy as String, Symbol or Proc", + Proc.new { |child| child.foo? }) + end + + context "with allow_destroy as lambda" do + it_behaves_like( + "has many with allow_destroy as String, Symbol or Proc", + lambda { |child| child.foo? }) + end + + context "with allow_destroy as any other expression that evaluates to true" do + let :body do + s = self + build_form({ url: "/categories" }, Category.new) do |f| + s.instance_exec do + allow(f.object.posts.build).to receive(:new_record?).and_return(false) + end + f.has_many :posts, allow_destroy: Object.new do |p| + p.input :title + end + end + end + + it_behaves_like "has many with allow_destroy = true", 0 + end + end + + context "with a new post" do + context "with allow_destroy = true" do + let :body do + build_form({ url: "/categories" }, Category.new) do |f| + f.object.posts.build + f.has_many :posts, allow_destroy: true do |p| + p.input :title + end + end + end + + it_behaves_like "has many with allow_destroy = false", 0 end end end @@ -608,7 +852,7 @@ def user # TODO: it doesn't make any sense to use your foreign key as something that's sortable (and therefore editable) context "with a new post" do let :body do - build_form({url: '/categories'}, Category.new) do |f| + build_form({ url: "/categories" }, Category.new) do |f| f.object.posts.build f.has_many :posts, sortable: :position do |p| p.input :title @@ -617,14 +861,13 @@ def user end it "shows the nested fields for unsaved records" do - expect(body).to have_selector("fieldset.inputs.has_many_fields") + expect(body).to have_css("fieldset.inputs.has-many-fields") end - end context "with post returning nil for the sortable attribute" do let :body do - build_form({url: '/categories'}, Category.new) do |f| + build_form({ url: "/categories" }, Category.new) do |f| f.object.posts.build position: 3 f.object.posts.build f.has_many :posts, sortable: :position do |p| @@ -634,20 +877,19 @@ def user end it "shows the nested fields for unsaved records" do - expect(body).to have_selector("fieldset.inputs.has_many_fields") + expect(body).to have_css("fieldset.inputs.has-many-fields") end - end context "with existing and new posts" do let! :category do - Category.create name: 'Name' + Category.create name: "Name" end let! :post do category.posts.create end let :body do - build_form({url: '/categories'}, category) do |f| + build_form({ url: "/categories" }, category) do |f| f.object.posts.build f.has_many :posts, sortable: :position do |p| p.input :title @@ -656,13 +898,13 @@ def user end it "shows the nested fields for saved and unsaved records" do - expect(body).to have_selector("fieldset.inputs.has_many_fields") + expect(body).to have_css("fieldset.inputs.has-many-fields") end end context "without sortable_start set" do let :body do - build_form({url: '/categories'}, Category.new) do |f| + build_form({ url: "/categories" }, Category.new) do |f| f.object.posts.build f.has_many :posts, sortable: :position do |p| p.input :title @@ -671,13 +913,13 @@ def user end it "defaults to 0" do - expect(body).to have_selector("div.has_many_container[data-sortable-start='0']") + expect(body).to have_css("div.has-many-container[data-sortable-start='0']") end end context "with sortable_start set" do let :body do - build_form({url: '/categories'}, Category.new) do |f| + build_form({ url: "/categories" }, Category.new) do |f| f.object.posts.build f.has_many :posts, sortable: :position, sortable_start: 15 do |p| p.input :title @@ -686,7 +928,7 @@ def user end it "sets the data attribute" do - expect(body).to have_selector("div.has_many_container[data-sortable-start='15']") + expect(body).to have_css("div.has-many-container[data-sortable-start='15']") end end end @@ -694,7 +936,7 @@ def user describe "with nesting" do context "in an inputs block" do let :body do - build_form({url: '/categories'}, Category.new) do |f| + build_form({ url: "/categories" }, Category.new) do |f| f.inputs "Field Wrapper" do f.object.posts.build f.has_many :posts do |p| @@ -705,43 +947,48 @@ def user end it "should wrap the has_many fieldset in an li" do - expect(body).to have_selector("ol > li.has_many_container") + expect(body).to have_css("ol > li.has-many-container") end it "should have a direct fieldset child" do - expect(body).to have_selector("li.has_many_container > fieldset") + expect(body).to have_css("li.has-many-container > fieldset") end it "should not contain invalid li children" do - expect(body).not_to have_selector("div.has_many_container > li") + expect(body).to have_no_css("div.has-many-container > li") end end context "in another has_many block" do - let :body do - build_form({url: '/categories'}, Category.new) do |f| + let :body_html do + form_html({ url: "/categories" }, Category.new) do |f| f.object.posts.build f.has_many :posts do |p| p.object.taggings.build + p.input :title + p.has_many :taggings do |t| t.input :tag + t.input :position end end end end + let(:body) { Capybara.string body_html } - it "should wrap the inner has_many fieldset in an ol > li" do - expect(body).to have_selector(".has_many_container ol > li.has_many_container > fieldset") + it "displays the input between the outer and inner has_many" do + expect(body).to have_css(".has-many-container ol > li:first-child input#category_posts_attributes_0_title") + expect(body).to have_css(".has-many-container ol > li:nth-child(2).has-many-container > fieldset") end it "should not contain invalid li children" do - expect(body).not_to have_selector(".has_many_container div.has_many_container > li") + expect(body).to have_no_css(".has-many-container div.has-many-container > li") end end end it "should render the block if it returns nil" do - body = build_form({url: '/categories'}, Category.new) do |f| + body = build_form({ url: "/categories" }, Category.new) do |f| f.object.posts.build f.has_many :posts do |p| p.input :title @@ -749,64 +996,30 @@ def user end end - expect(body).to have_selector("input[name='category[posts_attributes][0][title]']") + expect(body).to have_field("category[posts_attributes][0][title]") end end { # Testing that the same input can be used multiple times - "f.input :title, as: :string" => "post_title", - "f.input :title, as: :text" => "post_title", - "f.input :created_at, as: :time_select" => "post_created_at_2i", + "f.input :title, as: :string" => "post_title", + "f.input :title, as: :text" => "post_title", + "f.input :created_at, as: :time_select" => "post_created_at_2i", "f.input :created_at, as: :datetime_select" => "post_created_at_2i", - "f.input :created_at, as: :date_select" => "post_created_at_2i", + "f.input :created_at, as: :date_select" => "post_created_at_2i", # Testing that return values don't screw up the form - "f.input :title; nil" => "post_title", - "f.input :title; []" => "post_title", - "[:title].each{ |r| f.input r }" => "post_title", - "[:title].map { |r| f.input r }" => "post_title", + "f.input :title; nil" => "post_title", + "f.input :title; []" => "post_title", + "[:title].each{ |r| f.input r }" => "post_title", + "[:title].map { |r| f.input r }" => "post_title", }.each do |source, selector| - it "should properly buffer `#{source}`" do - body = build_form do |f| - f.inputs do - eval source - eval source - end - end - expect(body).to have_selector("[id=#{selector}]", count: 2) - end - end - - describe "datepicker input" do - context 'with default options' do - let :body do - build_form do |f| - f.inputs do - f.input :created_at, as: :datepicker - end - end - end - it "should generate a text input with the class of datepicker" do - expect(body).to have_selector("input.datepicker[type=text][name='post[created_at]']") - end - end - - context 'with date range options' do - let :body do - build_form do |f| - f.inputs do - f.input :created_at, as: :datepicker, - datepicker_options: { - min_date: Date.new(2013, 10, 18), - max_date: "2013-12-31" } - end + it "should properly buffer `#{source}`" do + body = build_form do |f| + f.inputs do + eval source + eval source end end - - it 'should generate a datepicker text input with data min and max dates' do - selector = "input.datepicker[type=text][name='post[created_at]']" - expect(body).to have_selector(selector) - expect(body.find(selector)["data-datepicker-options"]).to eq({ minDate: '2013-10-18', maxDate: '2013-12-31' }.to_json) - end + expect(body).to have_css("[id=#{selector}]", count: 2, visible: :all) end end end diff --git a/spec/unit/generators/install_spec.rb b/spec/unit/generators/install_spec.rb index 868d7460897..737cae36925 100644 --- a/spec/unit/generators/install_spec.rb +++ b/spec/unit/generators/install_spec.rb @@ -1,24 +1,20 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe "AA installation" do - context "should create" do - - it "active_admin.scss" do - path = Rails.root + "app/assets/stylesheets/active_admin.scss" - expect(File.exists? path).to be_truthy - end - - it "active_admin.js.coffee" do - expect(File.exists?(Rails.root + "app/assets/javascripts/active_admin.js.coffee")).to be_truthy - end +RSpec.describe "ActiveAdmin Installation" do + it "creates active_admin.css" do + expect(Rails.root.join("app/assets/stylesheets/active_admin.css")).to exist + end - it "the dashboard" do - expect(File.exists?(Rails.root + "app/admin/dashboard.rb")).to be_truthy - end + it "creates tailwind config file" do + expect(Rails.root.join("tailwind-active_admin.config.js")).to exist + end - it "the initializer" do - expect(File.exists?(Rails.root + "config/initializers/active_admin.rb")).to be_truthy - end + it "creates the dashboard resource" do + expect(Rails.root.join("app/admin/dashboard.rb")).to exist + end + it "creates the config initializer" do + expect(Rails.root.join("config/initializers/active_admin.rb")).to exist end end diff --git a/spec/unit/helpers/collection_spec.rb b/spec/unit/helpers/collection_spec.rb deleted file mode 100644 index b8915c250c6..00000000000 --- a/spec/unit/helpers/collection_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::Helpers::Collection do - - include ActiveAdmin::Helpers::Collection - - before(:all) do - Post.delete_all - Post.create!(title: "A post") - Post.create!(title: "A post") - Post.create!(title: "An other post") - end - - after(:all) do - Post.delete_all - end - - describe "#collection_size" do - it "should return the collection size for an ActiveRecord class" do - expect(collection_size(Post.where(nil))).to eq 3 - end - - it "should return the collection size for an ActiveRecord::Relation" do - expect(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(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(collection_size(Post.select("title, count(*) as nb_posts").group(:title).order("nb_posts"))).to eq 2 - end - - it "should take the defined collection by default" do - def collection; Post.where(nil); end - - expect(collection_size).to eq 3 - - def collection; Post.where(title: "An other post"); end - - expect(collection_size).to eq 1 - end - end - - describe "#collection_is_empty?" do - it "should return true when the collection is empty" do - expect(collection_is_empty?(Post.where(title: "Non existing post"))).to be_truthy - end - - it "should return false when the collection is not empty" do - expect(collection_is_empty?(Post.where(title: "A post"))).to be_falsey - end - - it "should take the defined collection by default" do - def collection; Post.where(nil); end - - expect(collection_is_empty?).to be_falsey - - def collection; Post.where(title: "Non existing post"); end - - expect(collection_is_empty?).to be_truthy - end - end -end diff --git a/spec/unit/helpers/scope_chain_spec.rb b/spec/unit/helpers/scope_chain_spec.rb index 47f107f0e06..26d35955cd8 100644 --- a/spec/unit/helpers/scope_chain_spec.rb +++ b/spec/unit/helpers/scope_chain_spec.rb @@ -1,7 +1,7 @@ -require 'rails_helper' - -describe ActiveAdmin::ScopeChain do +# frozen_string_literal: true +require "rails_helper" +RSpec.describe ActiveAdmin::ScopeChain do include ActiveAdmin::ScopeChain describe "#scope_chain" do @@ -25,7 +25,7 @@ end context "when Scope has a name and a scope block" do - let(:scope) { ActiveAdmin::Scope.new("My Scope"){|s| :scoped_relation } } + let(:scope) { ActiveAdmin::Scope.new("My Scope") { |s| :scoped_relation } } it "should instance_exec the block and return it" do expect(scope_chain(scope, relation)).to eq :scoped_relation @@ -33,4 +33,3 @@ end end end - diff --git a/spec/unit/helpers/settings_spec.rb b/spec/unit/helpers/settings_spec.rb deleted file mode 100644 index aceea47dc5c..00000000000 --- a/spec/unit/helpers/settings_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'rails_helper' -require 'active_admin/helpers/settings' - -describe ActiveAdmin::Settings do - - # A Class with settings module included - let(:klass) do - Class.new do - include ActiveAdmin::Settings - end - end - - it "should add a new setting with a default" do - klass.setting :my_setting, "Hello World" - expect(klass.default_settings[:my_setting]).to eq "Hello World" - end - - it "should initialize the defaults" do - klass.setting :my_setting, "Hello World" - expect(klass.new.my_setting).to eq "Hello World" - end - - it "should support settings of nil" do - klass.setting :my_setting, :some_val - inst = klass.new - inst.my_setting = nil - expect(inst.my_setting).to eq nil - end - -end diff --git a/spec/unit/i18n_spec.rb b/spec/unit/i18n_spec.rb deleted file mode 100644 index 0480e4f1f0e..00000000000 --- a/spec/unit/i18n_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'rails_helper' - -Dir.glob('config/locales/*.yml') do |locale_file| - 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 diff --git a/spec/unit/localizers/resource_localizer_spec.rb b/spec/unit/localizers/resource_localizer_spec.rb new file mode 100644 index 00000000000..55a7ef6531e --- /dev/null +++ b/spec/unit/localizers/resource_localizer_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.shared_examples_for "ActiveAdmin::Localizers::ResourceLocalizer" do + it "should use proper translation" do + string = ActiveAdmin::Localizers::ResourceLocalizer.t(action, model: model, model_name: model_name) + expect(string).to eq translation + end + + it "should accessible via ActiveAdmin::Localizers" do + resource = double(resource_label: model, resource_name: double(i18n_key: model_name)) + localizer = ActiveAdmin::Localizers.resource(resource) + expect(localizer.t(action)).to eq translation + end +end + +RSpec.describe ActiveAdmin::Localizers::ResourceLocalizer do + let(:action) { "new_model" } + let(:model) { "Comment" } + let(:model_name) { "comment" } + + it_behaves_like "ActiveAdmin::Localizers::ResourceLocalizer" do + let(:translation) { "New Comment" } + end + + describe "model action specified" do + around do |example| + with_translation %i[active_admin resources comment new_model], "Write comment" do + example.call + end + end + + it_behaves_like "ActiveAdmin::Localizers::ResourceLocalizer" do + let(:translation) { "Write comment" } + end + end +end diff --git a/spec/unit/menu_collection_spec.rb b/spec/unit/menu_collection_spec.rb index 9616c4d8f7f..3de6516a450 100644 --- a/spec/unit/menu_collection_spec.rb +++ b/spec/unit/menu_collection_spec.rb @@ -1,11 +1,10 @@ -require 'rails_helper' - -describe ActiveAdmin::MenuCollection do +# frozen_string_literal: true +require "rails_helper" +RSpec.describe ActiveAdmin::MenuCollection do let(:menus) { ActiveAdmin::MenuCollection.new } describe "#add" do - it "should initialize a new menu when first item" do menus.add :default, label: "Hello World" @@ -19,32 +18,27 @@ expect(menus.fetch(:default).items.size).to eq 2 end - end describe "#clear!" do - it "should remove all menus" do menus.add :default, label: "Hello World" menus.clear! - expect { + expect do menus.fetch(:non_default_menu) - }.to raise_error(ActiveAdmin::NoMenuError) - + end.to raise_error(ActiveAdmin::NoMenuError) end - end describe "#on_build" do - it "runs a callback when fetching a menu" do menus.on_build do |m| m.add :default, label: "Hello World" end - expect(menus.fetch(:default)["Hello World"]).to_not be_nil + expect(menus.fetch(:default)["Hello World"]).to_not eq nil end it "re-runs the callbacks when the menu is cleared" do @@ -52,11 +46,9 @@ m.add :default, label: "Hello World" end - expect(menus.fetch(:default)["Hello World"]).to_not be_nil + expect(menus.fetch(:default)["Hello World"]).to_not eq nil menus.clear! - expect(menus.fetch(:default)["Hello World"]).to_not be_nil + expect(menus.fetch(:default)["Hello World"]).to_not eq nil end - end - end diff --git a/spec/unit/menu_item_spec.rb b/spec/unit/menu_item_spec.rb index b8da10a353c..06c9f62ebf6 100644 --- a/spec/unit/menu_item_spec.rb +++ b/spec/unit/menu_item_spec.rb @@ -1,10 +1,10 @@ -require 'spec_helper' -require 'active_admin/menu' -require 'active_admin/menu_item' +# frozen_string_literal: true +require "rails_helper" +require "active_admin/menu" +require "active_admin/menu_item" module ActiveAdmin - describe MenuItem do - + RSpec.describe MenuItem do it "should have a label" do item = MenuItem.new(label: "Dashboard") expect(item.label).to eq "Dashboard" @@ -20,30 +20,14 @@ module ActiveAdmin expect(item.priority).to eq 10 end - context "conditional display" do - it "should store a Proc internally and evaluate it when requested" do - item = MenuItem.new - expect(item.instance_variable_get(:@should_display)).to be_a Proc - expect(item.display?).to_not be_a Proc - end - - it "should show the item by default" do - expect(MenuItem.new.display?).to eq true - end - - it "should hide the item" do - expect(MenuItem.new(if: proc{false}).display?).to eq false - end - end - it "should default to an empty hash for html_options" do item = MenuItem.new expect(item.html_options).to be_empty end it "should accept an options hash for link_to" do - item = MenuItem.new html_options: { target: :blank } - expect(item.html_options).to include(target: :blank) + item = MenuItem.new html_options: { target: "_blank" } + expect(item.html_options).to include(target: "_blank") end context "with no items" do @@ -54,7 +38,7 @@ module ActiveAdmin it "should accept new children" do item = MenuItem.new label: "Dashboard" - item.add label: "My Child Dashboard" + item.add label: "My Child Dashboard" expect(item.items.first).to be_a MenuItem expect(item.items.first.label).to eq "My Child Dashboard" end @@ -76,15 +60,7 @@ module ActiveAdmin end it "should give access to the menu item as an array" do - expect(item['Blog'].label).to eq 'Blog' - end - - it "should sort items based on priority and name" do - expect(item.items[0].label).to eq 'Users' - expect(item.items[1].label).to eq 'Settings' - expect(item.items[2].label).to eq 'Blog' - expect(item.items[3].label).to eq 'Cars' - expect(item.items[4].label).to eq 'Analytics' + expect(item["Blog"].label).to eq "Blog" end it "children should hold a reference to their parent" do @@ -92,52 +68,14 @@ module ActiveAdmin end end - describe "accessing ancestory" do - let(:item){ MenuItem.new label: "Blog" } - - context "with no parent" do - it "should return an empty array" do - expect(item.ancestors).to eq [] - end - end - - context "with one parent" do - let(:sub_item) do - item.add label: "Create New" - item["Create New"] - end - it "should return an array with the parent" do - expect(sub_item.ancestors).to eq [item] - end - end - - context "with many parents" do - before(:each) do - c1 = {label: "C1"} - c2 = {label: "C2"} - c3 = {label: "C3"} - - item.add(c1).add(c2).add(c3) - - item - end - let(:sub_item){ item["C1"]["C2"]["C3"] } - it "should return an array with the parents in reverse order" do - expect(sub_item.ancestors).to eq [item["C1"]["C2"], item["C1"], item] - end - end - end # accessing ancestory - - describe "#id" do it "should be normalized" do expect(MenuItem.new(id: "Foo Bar").id).to eq "foo_bar" end it "should not accept Procs" do - expect{ MenuItem.new(id: proc{"Dynamic"}).id }.to raise_error TypeError + expect { MenuItem.new(id: proc { "Dynamic" }).id }.to raise_error TypeError end end - end end diff --git a/spec/unit/menu_spec.rb b/spec/unit/menu_spec.rb index b553804e5c7..84d26bf4022 100644 --- a/spec/unit/menu_spec.rb +++ b/spec/unit/menu_spec.rb @@ -1,11 +1,11 @@ -require 'rails_helper' -require 'active_admin/menu' -require 'active_admin/menu_item' +# frozen_string_literal: true +require "rails_helper" +require "active_admin/menu" +require "active_admin/menu_item" include ActiveAdmin -describe ActiveAdmin::Menu do - +RSpec.describe ActiveAdmin::Menu do context "with no items" do it "should have an empty item collection" do menu = Menu.new @@ -28,7 +28,7 @@ end it "should give access to the menu item as an array" do - expect(menu['Dashboard'].label).to eq 'Dashboard' + expect(menu["Dashboard"].label).to eq "Dashboard" end end @@ -58,14 +58,22 @@ end end - describe "sorting items" do - it "should sort children by the result of their label proc" do - menu = Menu.new - menu.add label: proc{ "G" }, id: "not related 1" - menu.add label: proc{ "B" }, id: "not related 2" - menu.add label: proc{ "A" }, id: "not related 3" + describe "determining if node is current" do + let(:menu) { Menu.new } + let(:admin_item) { menu.add label: "Admin" } + let(:users_item) { menu.add parent: "Admin", label: "Users" } + let(:posts_item) { menu.add label: "Posts" } + + it "should consider item current in relation to itself" do + expect(admin_item.current?(admin_item)).to be true + end + + it "should consider item current in relation to a descendent" do + expect(admin_item.current?(users_item)).to be true + end - expect(menu.items.map(&:label)).to eq %w[A B G] + it "should not consider item current in relation to a non-self/non-descendant" do + expect(admin_item.current?(posts_item)).to be false end end end diff --git a/spec/unit/namespace/authorization_spec.rb b/spec/unit/namespace/authorization_spec.rb index 445359f76f2..4a09f45511c 100644 --- a/spec/unit/namespace/authorization_spec.rb +++ b/spec/unit/namespace/authorization_spec.rb @@ -1,15 +1,14 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Resource, "authorization" do - - let(:app){ ActiveAdmin::Application.new } - let(:namespace){ ActiveAdmin::Namespace.new(app, :admin) } - let(:auth){ double } +RSpec.describe ActiveAdmin::Resource, "authorization" do + let(:app) { ActiveAdmin::Application.new } + let(:namespace) { ActiveAdmin::Namespace.new(app, :admin) } + let(:auth) { double } describe "authorization_adapter" do - it "should return AuthorizationAdapter by default" do - expect(app.authorization_adapter).to eq ActiveAdmin::AuthorizationAdapter + expect(app.authorization_adapter).to eq ActiveAdmin::AuthorizationAdapter expect(namespace.authorization_adapter).to eq ActiveAdmin::AuthorizationAdapter end @@ -22,6 +21,5 @@ app.authorization_adapter = auth expect(app.authorization_adapter).to eq auth end - end end diff --git a/spec/unit/namespace/register_page_spec.rb b/spec/unit/namespace/register_page_spec.rb index ef679210522..6e9ea57497f 100644 --- a/spec/unit/namespace/register_page_spec.rb +++ b/spec/unit/namespace/register_page_spec.rb @@ -1,11 +1,10 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Namespace, "registering a page" do - - let(:application){ ActiveAdmin::Application.new } - - let(:namespace){ ActiveAdmin::Namespace.new(application, :admin) } - let(:menu){ namespace.fetch_menu(:default) } +RSpec.describe ActiveAdmin::Namespace, "registering a page" do + let(:application) { ActiveAdmin::Application.new } + let(:namespace) { ActiveAdmin::Namespace.new(application, :admin) } + let(:menu) { namespace.fetch_menu(:default) } context "with no configuration" do before do @@ -13,27 +12,25 @@ end it "should store the namespaced registered configuration" do - expect(namespace.resources.keys).to include('Status') + expect(namespace.resources.keys).to include("Status") end it "should create a new controller in the default namespace" do - expect(defined?(Admin::StatusController)).to be_truthy + expect(defined?(Admin::StatusController)).to eq "constant" end it "should create a menu item" do expect(menu["Status"]).to be_an_instance_of(ActiveAdmin::MenuItem) end - end # context "with no configuration" + end context "with a block configuration" do it "should be evaluated in the dsl" do - expect { - namespace.register_page "Status" do - raise "Hello World" - end - }.to raise_error + expect do |block| + namespace.register_page "Status", &block + end.to yield_control end - end # context "with a block configuration" + end describe "adding to the menu" do describe "adding as a top level item" do @@ -42,23 +39,25 @@ end it "should add a new menu item" do - expect(menu['Status']).to_not be_nil + expect(menu["Status"]).to_not eq nil end - end # describe "adding as a top level item" + end describe "adding as a child" do before do namespace.register_page "Status" do - menu parent: 'Extra' + menu parent: "Extra" end end + it "should generate the parent menu item" do - expect( menu['Extra']).to_not be_nil + expect(menu["Extra"]).to_not eq nil end + it "should generate its own child item" do - expect(menu['Extra']['Status']).to_not be_nil + expect(menu["Extra"]["Status"]).to_not eq nil end - end # describe "adding as a child" + end describe "disabling the menu" do before do @@ -66,9 +65,36 @@ menu false end end + it "should not create a menu item" do - expect(menu["Status"]).to be_nil + expect(menu["Status"]).to eq nil end end # describe "disabling the menu" end # describe "adding to the menu" + + describe "adding as a belongs to" do + context "when not optional" do + before do + namespace.register_page "Reports" do + belongs_to :author + end + end + + it "should be excluded from the menu" do + expect(menu["Reports"]).to be_nil + end + end + + context "when optional" do + before do + namespace.register_page "Reports" do + belongs_to :author, optional: true + end + end + + it "should be in the menu" do + expect(menu["Reports"]).to_not be_nil + end + end + end # describe "adding as a belongs to" end diff --git a/spec/unit/namespace/register_resource_spec.rb b/spec/unit/namespace/register_resource_spec.rb index 9a427e61e42..c034f998590 100644 --- a/spec/unit/namespace/register_resource_spec.rb +++ b/spec/unit/namespace/register_resource_spec.rb @@ -1,44 +1,53 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -# TODO: refactor this file so it doesn't depend on the Admin namespace in such a broken way. -# Specifically, the dashboard is already defined. +RSpec.describe ActiveAdmin::Namespace, "registering a resource" do + let(:application) { ActiveAdmin::Application.new } + let(:namespace) { ActiveAdmin::Namespace.new(application, :super_admin) } + let(:menu) { namespace.fetch_menu(:default) } -describe ActiveAdmin::Namespace, "registering a resource" do - - let(:application){ ActiveAdmin::Application.new } - - let(:namespace){ ActiveAdmin::Namespace.new(application, :admin) } - - let(:menu){ namespace.fetch_menu(:default) } + after { namespace.unload! } context "with no configuration" do before do namespace.register Category end + it "should store the namespaced registered configuration" do - expect(namespace.resources.keys).to include('Category') + expect(namespace.resources.keys).to include("Category") end + it "should create a new controller in the default namespace" do - expect(defined?(Admin::CategoriesController)).to be_truthy + expect(defined?(SuperAdmin::CategoriesController)).to eq "constant" end - skip "should not create the dashboard controller" do - defined?(Admin::DashboardController).to_not be_truthy + + it "should not create the dashboard controller" do + expect(defined?(SuperAdmin::DashboardController)).to_not eq "constant" end + it "should create a menu item" do expect(menu["Categories"]).to be_a ActiveAdmin::MenuItem expect(menu["Categories"].instance_variable_get(:@url)).to be_a Proc end - end # context "with no configuration" + end + + context "when resource class is a string" do + before do + namespace.register 'Category' + end + + it "should store the namespaced registered configuration" do + expect(namespace.resources.keys).to include("Category") + end + end context "with a block configuration" do it "should be evaluated in the dsl" do - expect { - namespace.register Category do - raise "Hello World" - end - }.to raise_error + expect do |block| + namespace.register Category, &block + end.to yield_control end - end # context "with a block configuration" + end context "with a resource that's namespaced" do before do @@ -47,17 +56,19 @@ module ::Mock; class Resource; def self.has_many(arg1, arg2); end; end; end end it "should store the namespaced registered configuration" do - expect(namespace.resources.keys).to include('Mock::Resource') + expect(namespace.resources.keys).to include("Mock::Resource") end + it "should create a new controller in the default namespace" do - expect(defined?(Admin::MockResourcesController)).to be_truthy + expect(defined?(SuperAdmin::MockResourcesController)).to eq "constant" end + it "should create a menu item" do expect(menu["Mock Resources"]).to be_an_instance_of(ActiveAdmin::MenuItem) end it "should use the resource as the model in the controller" do - expect(Admin::MockResourcesController.resource_class).to eq Mock::Resource + expect(SuperAdmin::MockResourcesController.resource_class).to eq Mock::Resource end end # context "with a resource that's namespaced" @@ -67,7 +78,7 @@ module ::Mock; class Resource; def self.has_many(arg1, arg2); end; end; end expect(namespace.resource_for(Post)).to eq post end - it 'should return nil when the resource has not been registered' do + it "should return nil when the resource has not been registered" do expect(namespace.resource_for(Post)).to eq nil end @@ -77,7 +88,7 @@ module ::Mock; class Resource; def self.has_many(arg1, arg2); end; end; end end it "should return the resource if it and it's parent were registered" do - user = namespace.register User + namespace.register User publisher = namespace.register Publisher expect(namespace.resource_for(Publisher)).to eq publisher end @@ -89,21 +100,22 @@ module ::Mock; class Resource; def self.has_many(arg1, arg2); end; end; end namespace.register Category end it "should add a new menu item" do - expect(menu['Categories']).to_not be_nil + expect(menu["Categories"]).to_not eq nil end end # describe "adding as a top level item" describe "adding as a child" do before do namespace.register Category do - menu parent: 'Blog' + menu parent: "Blog" end end it "should generate the parent menu item" do - expect(menu['Blog']).to_not be_nil + expect(menu["Blog"]).to_not eq nil end + it "should generate its own child item" do - expect(menu['Blog']['Categories']).to_not be_nil + expect(menu["Blog"]["Categories"]).to_not eq nil end end # describe "adding as a child" @@ -114,7 +126,7 @@ module ::Mock; class Resource; def self.has_many(arg1, arg2); end; end; end end end it "should not create a menu item" do - expect(menu["Categories"]).to be_nil + expect(menu["Categories"]).to eq nil end end # describe "disabling the menu" @@ -126,9 +138,10 @@ module ::Mock; class Resource; def self.has_many(arg1, arg2); end; end; end end end it "should not show up in the menu" do - expect(menu["Posts"]).to be_nil + expect(menu["Posts"]).to eq nil end end + context "when optional" do before do namespace.register Post do @@ -136,7 +149,7 @@ module ::Mock; class Resource; def self.has_many(arg1, arg2); end; end; end end end it "should show up in the menu" do - expect(menu["Posts"]).to_not be_nil + expect(menu["Posts"]).to_not eq nil end end end @@ -147,14 +160,15 @@ module ::Mock; class Resource; def self.has_many(arg1, arg2); end; end; end it "should be namespaced" do namespace = ActiveAdmin::Namespace.new(application, :one) namespace.register Category - expect(defined?(One::CategoriesController)).to be_truthy + expect(defined?(One::CategoriesController)).to eq "constant" end end + context "when not namespaced" do it "should not be namespaced" do namespace = ActiveAdmin::Namespace.new(application, :two) namespace.register Category - expect(defined?(Two::CategoriesController)).to be_truthy + expect(defined?(Two::CategoriesController)).to eq "constant" end end end # describe "dashboard controller name" diff --git a/spec/unit/namespace_spec.rb b/spec/unit/namespace_spec.rb index 1f9a1becf46..20bab759921 100644 --- a/spec/unit/namespace_spec.rb +++ b/spec/unit/namespace_spec.rb @@ -1,11 +1,11 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Namespace do - - let(:application){ ActiveAdmin::Application.new } +RSpec.describe ActiveAdmin::Namespace do + let(:application) { ActiveAdmin::Application.new } context "when new" do - let(:namespace){ ActiveAdmin::Namespace.new(application, :admin) } + let(:namespace) { ActiveAdmin::Namespace.new(application, :admin) } it "should have an application instance" do expect(namespace.application).to eq application @@ -24,11 +24,39 @@ end end # context "when new" + describe "#unload!" do + context "when controller is only defined without a namespace" do + before do + # To ensure Admin::PostsController is defined + ActiveAdmin.register Post + + # To ensure ::PostsController is defined + ActiveAdmin.register Post, namespace: false + + # To prevent unload! from unregistering ::PostsController + ActiveAdmin.application.namespaces.instance_variable_get(:@namespaces).delete(:root) + + # To force Admin::PostsController to not be there + Admin.send(:remove_const, "PostsController") + end + + after do + load_resources {} + end + + it "should not crash" do + expect { ActiveAdmin.unload! }.not_to raise_error + end + end + end + describe "settings" do - let(:namespace){ ActiveAdmin::Namespace.new(application, :admin) } + let(:namespace) { ActiveAdmin::Namespace.new(application, :admin) } it "should inherit the site title from the application" do - ActiveAdmin::Namespace.setting :site_title, "Not the Same" + ActiveAdmin.deprecator.silence do + ActiveAdmin::Namespace.setting :site_title, "Not the Same" + end expect(namespace.site_title).to eq application.site_title end @@ -39,34 +67,29 @@ end end - describe "#fetch_menu" do - let(:namespace){ ActiveAdmin::Namespace.new(application, :admin) } + let(:namespace) { ActiveAdmin::Namespace.new(application, :admin) } it "returns the menu" do expect(namespace.fetch_menu(:default)).to be_an_instance_of(ActiveAdmin::Menu) end - it "should have utility nav menu" do - expect(namespace.fetch_menu(:utility_navigation)).to be_an_instance_of(ActiveAdmin::Menu) - end - it "should raise an exception if the menu doesn't exist" do - expect { + expect do namespace.fetch_menu(:not_a_menu_that_exists) - }.to raise_error(KeyError) + end.to raise_error(KeyError) end end describe "#build_menu" do - let(:namespace){ ActiveAdmin::Namespace.new(application, :admin) } + let(:namespace) { ActiveAdmin::Namespace.new(application, :admin) } it "should set the block as a menu build callback" do namespace.build_menu do |menu| menu.add label: "menu item" end - expect(namespace.fetch_menu(:default)["menu item"]).to_not be_nil + expect(namespace.fetch_menu(:default)["menu item"]).to_not eq nil end it "should set a block on a custom menu" do @@ -74,30 +97,7 @@ menu.add label: "menu item" end - expect(namespace.fetch_menu(:test)["menu item"]).to_not be_nil + expect(namespace.fetch_menu(:test)["menu item"]).to_not eq nil end end - - describe "utility navigation" do - let(:namespace){ ActiveAdmin::Namespace.new(application, :admin) } - let(:menu) do - namespace.build_menu :utility_navigation do |menu| - menu.add label: "ActiveAdmin.info", url: "http://www.activeadmin.info", html_options: { target: :blank } - namespace.add_logout_button_to_menu menu, 1, class: "matt" - end - namespace.fetch_menu(:utility_navigation) - end - - it "should have a logout button to the far left" do - expect(menu["Logout"]).to_not be_nil - expect(menu["Logout"].priority).to eq 1 - end - - it "should have a static link with a target of :blank" do - expect(menu["ActiveAdmin.info"]).to_not be_nil - expect(menu["ActiveAdmin.info"].html_options).to include(target: :blank) - end - - end - end diff --git a/spec/unit/order_clause_spec.rb b/spec/unit/order_clause_spec.rb index ab518f80f4b..872962605cb 100644 --- a/spec/unit/order_clause_spec.rb +++ b/spec/unit/order_clause_spec.rb @@ -1,49 +1,59 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::OrderClause do - subject { described_class.new clause } +RSpec.describe ActiveAdmin::OrderClause do + subject { described_class.new(config, clause) } let(:application) { ActiveAdmin::Application.new } - let(:namespace) { ActiveAdmin::Namespace.new application, :admin } - let(:config) { ActiveAdmin::Resource.new namespace, Post } + let(:namespace) { ActiveAdmin::Namespace.new application, :admin } + let(:config) { ActiveAdmin::Resource.new namespace, Post } - describe 'id_asc (existing column)' do - let(:clause) { 'id_asc' } + describe "id_asc (existing column)" do + let(:clause) { "id_asc" } it { is_expected.to be_valid } - describe '#field' do + describe "#field" do subject { super().field } - it { is_expected.to eq('id') } + it { is_expected.to eq("id") } end - describe '#order' do + describe "#order" do subject { super().order } - it { is_expected.to eq('asc') } + it { is_expected.to eq("asc") } end - specify '#to_sql prepends table name' do - expect(subject.to_sql(config)).to eq '"posts"."id" asc' + specify "#to_sql prepends table name" do + expect(subject.to_sql).to eq '"posts"."id" asc' end end - describe 'virtual_column_asc' do - let(:clause) { 'virtual_column_asc' } + describe "posts.id_asc" do + let(:clause) { "posts.id_asc" } + + describe "#table_column" do + subject { super().table_column } + it { is_expected.to eq("posts.id") } + end + end + + describe "virtual_column_asc" do + let(:clause) { "virtual_column_asc" } it { is_expected.to be_valid } - describe '#field' do + describe "#field" do subject { super().field } - it { is_expected.to eq('virtual_column') } + it { is_expected.to eq("virtual_column") } end - describe '#order' do + describe "#order" do subject { super().order } - it { is_expected.to eq('asc') } + it { is_expected.to eq("asc") } end - specify '#to_sql' do - expect(subject.to_sql(config)).to eq '"virtual_column" asc' + specify "#to_sql" do + expect(subject.to_sql).to eq '"virtual_column" asc' end end @@ -52,28 +62,28 @@ it { is_expected.to be_valid } - describe '#field' do + describe "#field" do subject { super().field } it { is_expected.to eq("hstore_col->'field'") } end - describe '#order' do + describe "#order" do subject { super().order } - it { is_expected.to eq('desc') } + it { is_expected.to eq("desc") } end - it 'converts to sql' do - expect(subject.to_sql(config)).to eq %Q("hstore_col"->'field' desc) + it "converts to sql" do + expect(subject.to_sql).to eq %Q("hstore_col"->'field' desc) end end - describe '_asc' do - let(:clause) { '_asc' } + describe "_asc" do + let(:clause) { "_asc" } it { is_expected.not_to be_valid } end - describe 'nil' do + describe "nil" do let(:clause) { nil } it { is_expected.not_to be_valid } diff --git a/spec/unit/page_controller_spec.rb b/spec/unit/page_controller_spec.rb deleted file mode 100644 index 0d5ad5465c2..00000000000 --- a/spec/unit/page_controller_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::PageController do - let(:controller) { ActiveAdmin::PageController.new } -end diff --git a/spec/unit/page_spec.rb b/spec/unit/page_spec.rb index 3c487b2753c..8d241e99ac1 100644 --- a/spec/unit/page_spec.rb +++ b/spec/unit/page_spec.rb @@ -1,16 +1,14 @@ -# encoding: utf-8 - -require 'rails_helper' -require File.expand_path('config_shared_examples', File.dirname(__FILE__)) +# frozen_string_literal: true +require "rails_helper" +require File.expand_path("config_shared_examples", __dir__) module ActiveAdmin - describe Page do - + RSpec.describe Page do it_should_behave_like "ActiveAdmin::Resource" - before { load_defaults! } - let(:application){ ActiveAdmin::Application.new } - let(:namespace){ Namespace.new(application, :admin) } + let(:application) { ActiveAdmin::Application.new } + let(:namespace) { Namespace.new(application, :admin) } + let(:page_name) { "Chocolate I lØve You!" } def config(options = {}) @config ||= namespace.register_page("Chocolate I lØve You!", options) @@ -20,8 +18,9 @@ def config(options = {}) it "should return a namespaced controller name" do expect(config.controller_name).to eq "Admin::ChocolateILoveYouController" end + context "when non namespaced controller" do - let(:namespace){ ActiveAdmin::Namespace.new(application, :root) } + let(:namespace) { ActiveAdmin::Namespace.new(application, :root) } it "should return a non namespaced controller name" do expect(config.controller_name).to eq "ChocolateILoveYouController" end @@ -34,7 +33,7 @@ def config(options = {}) end it "returns the singular, lowercase name" do - expect(config.resource_name.singular).to eq "chocolate i lØve you!" + expect(config.resource_name.singular).to eq "chocolate i løve you!" end end @@ -74,5 +73,52 @@ def config(options = {}) expect(config.sidebar_sections?).to eq false end + context "with belongs to config" do + let!(:post_config) { namespace.register Post } + let!(:page_config) do + namespace.register_page page_name do + belongs_to :post + end + end + + it "configures page with belongs_to" do + expect(page_config.belongs_to?).to be true + end + + it "sets navigation menu to parent" do + expect(page_config.navigation_menu_name).to eq :post + end + + it "builds a belongs_to relationship" do + belongs_to = page_config.belongs_to_config + + expect(belongs_to.target).to eq(post_config) + expect(belongs_to.owner).to eq(page_config) + expect(belongs_to.optional?).to be_falsy + end + + it "forwards belongs_to call to controller" do + options = { optional: true } + expect(page_config.controller).to receive(:belongs_to).with(:post, options) + page_config.belongs_to :post, options + end + end # context "with belongs to config" do + + context "with optional belongs to config" do + let!(:post_config) { namespace.register Post } + let!(:page_config) do + namespace.register_page page_name do + belongs_to :post, optional: true + end + end + + it "does not override default navigation menu" do + expect(page_config.navigation_menu_name).to eq(:default) + end + end # context "with optional belongs to config" do + + it "has no belongs_to by default" do + expect(config.belongs_to?).to be_falsy + end end end diff --git a/spec/unit/pretty_format_spec.rb b/spec/unit/pretty_format_spec.rb deleted file mode 100644 index c1b3634d0f9..00000000000 --- a/spec/unit/pretty_format_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'rails_helper' - -describe "#pretty_format" do - include ActiveAdmin::ViewHelpers::DisplayHelper - - def method_missing(*args, &block) - mock_action_view.send *args, &block - end - - {String: 'hello', Fixnum: 23, Float: 5.67, Bignum: 10**30, Symbol: :foo, - 'Arbre::Element' => Arbre::Element.new.br(:foo) - }.each do |klass, obj| - it "should call `to_s` on #{klass}s" do - expect(obj).to be_a klass.to_s.constantize # safeguard for Bignum - expect(pretty_format(obj)).to eq obj.to_s - end - end - - context "given a Date or a Time" do - it "should return a localized Date or Time with long format" do - t = Time.now - expect(self).to receive(:localize).with(t, {format: :long}) { "Just Now!" } - expect(pretty_format(t)).to eq "Just Now!" - end - - context "actually do the formatting" do - it "should actually do the formatting" do - t = Time.utc(1985,"feb",28,20,15,1) - expect(pretty_format(t)).to eq "February 28, 1985 20:15" - end - - context "apply custom localize format" do - before do - ActiveAdmin.application.localize_format = :short - end - after do - ActiveAdmin.application = nil - end - it "should actually do the formatting" do - t = Time.utc(1985, "feb", 28, 20, 15, 1) - - expect(pretty_format(t)).to eq "28 Feb 20:15" - end - end - - context "with non-English locale" do - before(:all) do - @previous_locale = I18n.locale.to_s - I18n.locale = "es" - end - after(:all) do - I18n.locale = @previous_locale - end - it "should return a localized Date or Time with long format for non-english locale" do - t = Time.utc(1985,"feb",28,20,15,1) - expect(pretty_format(t)).to eq "28 de febrero de 1985 20:15" - end - end - end - end - - context "given an ActiveRecord object" do - it "should delegate to auto_link" do - post = Post.new - expect(self).to receive(:auto_link).with(post) { "model name" } - expect(pretty_format(post)).to eq "model name" - end - end - - context "given an arbitrary object" do - it "should delegate to `display_name`" do - something = Class.new.new - expect(self).to receive(:display_name).with(something) { "I'm not famous" } - expect(pretty_format(something)).to eq "I'm not famous" - end - end -end diff --git a/spec/unit/pundit_adapter_spec.rb b/spec/unit/pundit_adapter_spec.rb index a514e49ed94..d3b3296bb67 100644 --- a/spec/unit/pundit_adapter_spec.rb +++ b/spec/unit/pundit_adapter_spec.rb @@ -1,4 +1,5 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" class DefaultPolicy < ApplicationPolicy def respond_to_missing?(method, include_private = false) @@ -6,11 +7,7 @@ def respond_to_missing?(method, include_private = false) end def method_missing(method, *args, &block) - if method.to_s[0...3] == "foo" - method.to_s[4...7] == "yes" - else - super - end + method.to_s[4...7] == "yes" if method.to_s[0...3] == "foo" end class Scope @@ -28,14 +25,13 @@ def resolve end end -describe ActiveAdmin::PunditAdapter do - +RSpec.describe ActiveAdmin::PunditAdapter do describe "full integration" do - let(:application) { ActiveAdmin::Application.new } let(:namespace) { ActiveAdmin::Namespace.new(application, "Admin") } let(:resource) { namespace.register(Post) } - let(:auth) { namespace.authorization_adapter.new(resource, double) } + let(:user) { User.new } + let(:auth) { namespace.authorization_adapter.new(resource, user) } let(:default_policy_klass) { DefaultPolicy } let(:default_policy_klass_name) { "DefaultPolicy" } @@ -48,6 +44,16 @@ def resolve expect(auth.authorized?(:update, Post)).to eq false end + it "should allow differentiating between new and create" do + expect(auth.authorized?(:new, Post)).to eq true + expect(auth.authorized?(ActiveAdmin::Auth::NEW, Post)).to eq true + + announcement_category = Category.new(name: "Announcements") + announcement_post = Post.new(title: "Big announcement", category: announcement_category) + expect(auth.authorized?(:create, announcement_post)).to eq false + expect(auth.authorized?(ActiveAdmin::Auth::CREATE, announcement_post)).to eq false + end + it "should scope the collection" do class RSpec::Mocks::DoublePolicy < ApplicationPolicy class Scope < Struct.new(:user, :scope) @@ -64,12 +70,67 @@ def resolve it "works well with method_missing" do allow(auth).to receive(:retrieve_policy).and_return(DefaultPolicy.new(double, double)) - expect(auth.authorized?(:foo_no)).to be_falsey - expect(auth.authorized?(:foo_yes)).to be_truthy - expect(auth.authorized?(:bar_yes)).to be_falsey + expect(auth.authorized?(:foo_no)).to eq false + expect(auth.authorized?(:foo_yes)).to eq true + expect(auth.authorized?(:bar_yes)).to eq false + end + + context "when Pundit namespace provided" do + before do + allow(ActiveAdmin.application).to receive(:pundit_policy_namespace).and_return :foobar + end + + it "looks for a namespaced policy" do + expect(Pundit).to receive(:policy).with(anything, [:foobar, Post]).and_return(DefaultPolicy.new(double, double)) + auth.authorized?(:read, Post) + end + + it "looks for a namespaced policy scope" do + collection = double + expect(Pundit).to receive(:policy_scope!).with(anything, [:foobar, collection]).and_return(DefaultPolicy::Scope.new(double, double)) + auth.scope_collection(collection, :read) + end + + it "uses the resource when no subject given" do + expect(Pundit).to receive(:policy).with(anything, [:foobar, resource]).and_return(DefaultPolicy::Scope.new(double, double)) + auth.authorized?(:index) + end + end + + it "uses the resource when no subject given" do + expect(Pundit).to receive(:policy).with(anything, resource).and_return(DefaultPolicy::Scope.new(double, double)) + auth.authorized?(:index) end - context 'when Pundit is unable to find policy scope' do + context "when model name contains policy namespace name" do + include_context "capture stderr" + + before do + allow(ActiveAdmin.application).to receive(:pundit_policy_namespace).and_return :pub + namespace.register(Publisher) + ActiveAdmin.deprecator.behavior = :stderr + end + + after do + ActiveAdmin.deprecator.behavior = :raise + end + + it "looks for a namespaced policy" do + expect(Pundit).to receive(:policy).with(anything, [:pub, Publisher]).and_return(DefaultPolicy.new(double, double)) + auth.authorized?(:read, Publisher) + end + + it "fallbacks to the policy without namespace" do + expect(Pundit).to receive(:policy).with(anything, [:pub, Publisher]).and_return(nil) + expect(Pundit).to receive(:policy).with(anything, Publisher).and_return(DefaultPolicy.new(double, double)) + + auth.authorized?(:read, Publisher) + + expect($stderr.string).to include("ActiveAdmin was unable to find policy Pub::DefaultPolicy. DefaultPolicy will be used instead.") + end + end + + context "when Pundit is unable to find policy scope" do let(:collection) { double("collection", to_sym: :collection) } subject(:scope) { auth.scope_collection(collection, :read) } @@ -79,6 +140,14 @@ def resolve end it("should return default policy's scope if defined") { is_expected.to eq(collection) } + + context "and default policy doesn't exist" do + let(:default_policy_klass_name) { nil } + + it "raises the error" do + expect { subject }.to raise_error Pundit::NotDefinedError + end + end end context "when Pundit is unable to find policy" do @@ -88,11 +157,30 @@ def resolve before do allow(ActiveAdmin.application).to receive(:pundit_default_policy).and_return default_policy_klass_name - allow(Pundit).to receive(:policy!) { raise Pundit::NotDefinedError.new } + allow(Pundit).to receive(:policy) { nil } end it("should return default policy instance") { is_expected.to be_instance_of(default_policy_klass) } + + context "and default policy doesn't exist" do + let(:default_policy_klass_name) { nil } + + it "raises the error" do + expect { subject }.to raise_error Pundit::NotDefinedError + end + end end - end + context "when retrieve_policy is given a page and namespace is :active_admin" do + let(:page) { namespace.register_page "Dashboard" } + + subject(:policy) { auth.retrieve_policy(page) } + + before do + allow(ActiveAdmin.application).to receive(:pundit_policy_namespace).and_return :active_admin + end + + it("should return page policy instance") { is_expected.to be_instance_of(ActiveAdmin::PagePolicy) } + end + end end diff --git a/spec/unit/resource/action_items_spec.rb b/spec/unit/resource/action_items_spec.rb index 4974ae0bf13..0dd2617739d 100644 --- a/spec/unit/resource/action_items_spec.rb +++ b/spec/unit/resource/action_items_spec.rb @@ -1,14 +1,13 @@ -require 'rails_helper' - -describe ActiveAdmin::Resource::ActionItems do +# frozen_string_literal: true +require "rails_helper" +RSpec.describe ActiveAdmin::Resource::ActionItems do let(:resource) do namespace = ActiveAdmin::Namespace.new(ActiveAdmin::Application.new, :admin) namespace.register(Post) end describe "adding a new action item" do - before do resource.clear_action_items! resource.add_action_item :empty do @@ -25,13 +24,25 @@ end it "should store the block in the action item" do - expect(resource.action_items.first.block).to_not be_nil + expect(resource.action_items.first.block).to_not eq nil end + it "should be ordered by priority" do + resource.add_action_item :first, priority: 0 do + # Empty ... + end + resource.add_action_item :some_other do + # Empty ... + end + resource.add_action_item :second, priority: 1 do + # Empty ... + end + + expect(resource.action_items_for(:index).collect(&:name)).to eq [:first, :second, :empty, :some_other] + end end describe "setting an action item to only display on specific controller actions" do - before do resource.clear_action_items! resource.add_action_item :new, only: :index do @@ -44,11 +55,10 @@ it "should return only relevant action items" do expect(resource.action_items_for(:index).size).to eq 1 - expect { + expect do resource.action_items_for(:index).first.call - }.to raise_exception(StandardError) + end.to raise_exception(StandardError) end - end describe "default action items" do @@ -56,10 +66,9 @@ expect(resource.action_items.size).to eq 3 end - it 'can be removed by name' do + it "can be removed by name" do resource.remove_action_item :new expect(resource.action_items.size).to eq 2 end end - end diff --git a/spec/unit/resource/attributes_spec.rb b/spec/unit/resource/attributes_spec.rb new file mode 100644 index 00000000000..e20d15cae27 --- /dev/null +++ b/spec/unit/resource/attributes_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true +require "rails_helper" + +module ActiveAdmin + RSpec.describe Resource, "Attributes" do + let(:application) { ActiveAdmin::Application.new } + let(:namespace) { ActiveAdmin::Namespace.new application, :admin } + let(:resource_config) { ActiveAdmin::Resource.new namespace, Post } + + describe "#resource_attributes" do + subject do + resource_config.resource_attributes + end + + it "should return attributes hash" do + expect(subject).to eq( + author_id: :author, + body: :body, + created_at: :created_at, + custom_category_id: :category, + foo_id: :foo_id, + position: :position, + published_date: :published_date, + starred: :starred, + title: :title, + updated_at: :updated_at) + end + + it "does not return sensitive attributes" do + keep = ActiveAdmin.application.filter_attributes + ActiveAdmin.application.filter_attributes = [:published_date] + expect(subject).to_not include :published_date + ActiveAdmin.application.filter_attributes = keep + end + + context "when resource has a counter cache" do + subject { ActiveAdmin::Resource.new(namespace, Category).resource_attributes } + + it "should not include counter cache column" do + expect(subject.keys).not_to include(:posts_count) + end + end + + context "when resource has a 'counter cache'-like column" do + subject { ActiveAdmin::Resource.new(namespace, User).resource_attributes } + + it "should include that attribute" do + expect(subject).to include(sign_in_count: :sign_in_count) + end + end + end + + describe "#association_columns" do + subject do + resource_config.association_columns + end + + it "should return associations" do + expect(subject).to eq([:author, :category]) + end + end + + describe "#content_columns" do + subject do + resource_config.content_columns + end + + it "should return columns without associations" do + expect(subject).to eq([:title, :body, :published_date, :position, :starred, :foo_id, :created_at, :updated_at]) + end + end + end +end diff --git a/spec/unit/resource/comments_spec.rb b/spec/unit/resource/comments_spec.rb new file mode 100644 index 00000000000..85984d0279b --- /dev/null +++ b/spec/unit/resource/comments_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe "ActiveAdmin Comments", type: :controller do + before do + load_resources { ActiveAdmin.register ActiveAdmin::Comment, as: "Comment" } + @controller = Admin::CommentsController.new + end + + describe "#destroy" do + let(:user) { User.create! } + let(:comment) { ActiveAdmin::Comment.create!(body: "body", namespace: :admin, resource: user, author: user) } + + context "success" do + it "deletes comments and redirects to root fallback" do + delete :destroy, params: { id: comment.id } + + expect(response).to redirect_to admin_root_url + end + + it "deletes comments and redirects back" do + request.env["HTTP_REFERER"] = "/admin/users/1" + + delete :destroy, params: { id: comment.id } + + expect(response).to redirect_to "/admin/users/1" + end + end + + context "failure" do + it "does not delete comment on error and redirects to root fallback" do + expect(@controller).to receive(:destroy_resource) do |comment| + comment.errors.add(:body, :invalid) + end + + delete :destroy, params: { id: comment.id } + + expect(response).to redirect_to admin_root_url + end + + it "does not delete comment on error and redirects back" do + request.env["HTTP_REFERER"] = "/admin/users/1" + expect(@controller).to receive(:destroy_resource) do |comment| + comment.errors.add(:body, :invalid) + end + + delete :destroy, params: { id: comment.id } + + expect(response).to redirect_to "/admin/users/1" + end + end + end +end diff --git a/spec/unit/resource/includes_spec.rb b/spec/unit/resource/includes_spec.rb index f38542591b4..3af6557b9a7 100644 --- a/spec/unit/resource/includes_spec.rb +++ b/spec/unit/resource/includes_spec.rb @@ -1,13 +1,13 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" module ActiveAdmin - describe Resource, "Includes" do + RSpec.describe Resource, "Includes" do describe "#includes" do - let(:application) { ActiveAdmin::Application.new } let(:namespace) { ActiveAdmin::Namespace.new application, :admin } let(:resource_config) { ActiveAdmin::Resource.new namespace, Post } - let(:dsl){ ActiveAdmin::ResourceDSL.new(resource_config, Post) } + let(:dsl) { ActiveAdmin::ResourceDSL.new(resource_config) } it "should register the includes in the config" do dsl.run_registration_block do @@ -15,7 +15,6 @@ module ActiveAdmin end expect(resource_config.includes.size).to eq(2) end - end end end diff --git a/spec/unit/resource/menu_spec.rb b/spec/unit/resource/menu_spec.rb deleted file mode 100644 index 17f6ee7648b..00000000000 --- a/spec/unit/resource/menu_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'rails_helper' - -module ActiveAdmin - describe Resource, "Menu" do - - before { load_defaults! } - - let(:application){ ActiveAdmin::Application.new } - let(:namespace){ Namespace.new(application, :admin) } - - def config(options = {}) - @config ||= Resource.new(namespace, Category, options) - end - - # TODO... - - end -end diff --git a/spec/unit/resource/naming_spec.rb b/spec/unit/resource/naming_spec.rb index 727610eb777..bd96c766323 100644 --- a/spec/unit/resource/naming_spec.rb +++ b/spec/unit/resource/naming_spec.rb @@ -1,21 +1,16 @@ -# -*- coding: UTF-8 -*- - -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" module ActiveAdmin - describe Resource, "Naming" do - - before { load_defaults! } - - let(:application){ ActiveAdmin::Application.new } - let(:namespace){ Namespace.new(application, :admin) } + RSpec.describe Resource, "Naming" do + let(:application) { ActiveAdmin::Application.new } + let(:namespace) { Namespace.new(application, :admin) } def config(options = {}) @config ||= Resource.new(namespace, Category, options) end module ::Mock class Resource < ActiveRecord::Base; end; end - module NoActiveModel class Resource; end; end describe "singular resource name" do context "when class" do @@ -23,11 +18,13 @@ module NoActiveModel class Resource; end; end expect(config.resource_name.singular).to eq "category" end end + context "when a class in a module" do it "should underscore the module and the class" do expect(Resource.new(namespace, Mock::Resource).resource_name.singular).to eq "mock_resource" end end + context "when you pass the 'as' option" do it "should underscore the passed through string" do expect(config(as: "Blog Category").resource_name.singular).to eq "blog_category" @@ -86,7 +83,7 @@ module NoActiveModel class Resource; end; end context "when the :as option is given" do describe "singular label" do it "should translate the custom name" do - config = config(as: 'My Category') + config = config(as: "My Category") expect(config.resource_name).to receive(:translate).and_return "Translated category" expect(config.resource_label).to eq "Translated category" end @@ -94,13 +91,12 @@ module NoActiveModel class Resource; end; end describe "plural label" do it "should translate the custom name" do - config = config(as: 'My Category') + config = config(as: "My Category") expect(config.resource_name).to receive(:translate).at_least(:once).and_return "Translated categories" expect(config.plural_resource_label).to eq "Translated categories" end end end - end end @@ -110,7 +106,7 @@ module NoActiveModel class Resource; end; end [:==, :===, :eql?].each do |method| it "are equivalent when compared with #{method}" do - expect(resource_name.public_send(method, duplicate_resource_name)).to be_truthy + expect(resource_name.public_send(method, duplicate_resource_name)).to eq true end end diff --git a/spec/unit/resource/ordering_spec.rb b/spec/unit/resource/ordering_spec.rb new file mode 100644 index 00000000000..7f64bbdac85 --- /dev/null +++ b/spec/unit/resource/ordering_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +require "rails_helper" + +module ActiveAdmin + RSpec.describe Resource, "Ordering" do + describe "#order_by" do + let(:application) { ActiveAdmin::Application.new } + let(:namespace) { ActiveAdmin::Namespace.new application, :admin } + let(:resource_config) { ActiveAdmin::Resource.new namespace, Post } + let(:dsl) { ActiveAdmin::ResourceDSL.new(resource_config) } + + it "should register the ordering in the config" do + dsl.run_registration_block do + order_by(:age, &:to_sql) + end + expect(resource_config.ordering.size).to eq(1) + end + + it "should allow to setup custom ordering class" do + MyOrderClause = Class.new(ActiveAdmin::OrderClause) + dsl.run_registration_block do + config.order_clause = MyOrderClause + end + expect(resource_config.order_clause).to eq(MyOrderClause) + expect(application.order_clause).to eq(ActiveAdmin::OrderClause) + end + end + end +end diff --git a/spec/unit/resource/page_presenters_spec.rb b/spec/unit/resource/page_presenters_spec.rb index 166bdced717..41fb4177d4e 100644 --- a/spec/unit/resource/page_presenters_spec.rb +++ b/spec/unit/resource/page_presenters_spec.rb @@ -1,12 +1,12 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Resource::PagePresenters do - - let(:namespace){ ActiveAdmin::Namespace.new(ActiveAdmin::Application.new, :admin) } - let(:resource){ namespace.register(Post) } +RSpec.describe ActiveAdmin::Resource::PagePresenters do + let(:namespace) { ActiveAdmin::Namespace.new(ActiveAdmin::Application.new, :admin) } + let(:resource) { namespace.register(Post) } it "should have an empty set of configs on initialize" do - expect(resource.page_presenters).to eq ({}) + expect(resource.page_presenters).to eq({}) end it "should add a show page presenter" do @@ -16,13 +16,12 @@ end it "should add an index page presenter" do - page_presenter = ActiveAdmin::PagePresenter.new({as: :table}) + page_presenter = ActiveAdmin::PagePresenter.new({ as: :table }) resource.set_page_presenter(:index, page_presenter) expect(resource.page_presenters[:index].default).to eq page_presenter end describe "#get_page_presenter" do - it "should return a page config when set" do page_presenter = ActiveAdmin::PagePresenter.new resource.set_page_presenter(:index, page_presenter) @@ -38,7 +37,5 @@ it "should return nil when no page config set" do expect(resource.get_page_presenter(:index)).to eq nil end - end - end diff --git a/spec/unit/resource/pagination_spec.rb b/spec/unit/resource/pagination_spec.rb index 15be663844f..c4e39a7880f 100644 --- a/spec/unit/resource/pagination_spec.rb +++ b/spec/unit/resource/pagination_spec.rb @@ -1,12 +1,10 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" module ActiveAdmin - describe Resource, "Pagination" do - - before { load_defaults! } - - let(:application){ ActiveAdmin::Application.new } - let(:namespace){ Namespace.new(application, :admin) } + RSpec.describe Resource, "Pagination" do + let(:application) { ActiveAdmin::Application.new } + let(:namespace) { Namespace.new(application, :admin) } def config(options = {}) @config ||= Resource.new(namespace, Category, options) diff --git a/spec/unit/resource/routes_spec.rb b/spec/unit/resource/routes_spec.rb index c79e948b943..327ca8c367d 100644 --- a/spec/unit/resource/routes_spec.rb +++ b/spec/unit/resource/routes_spec.rb @@ -1,73 +1,132 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -module ActiveAdmin - describe Resource::Routes do - before { load_defaults! } +RSpec.describe ActiveAdmin::Resource::Routes do + let(:application) { ActiveAdmin.application } + let(:namespace) { application.namespace(:admin) } - describe "route names" do - context "when in the admin namespace" do - let!(:config) { ActiveAdmin.register Category } - let(:category) { Category.new { |c| c.id = 123 } } + context "when in the admin namespace" do + let(:config) { namespace.resource_for("Category") } - it "should return the route prefix" do - expect(config.route_prefix).to eq 'admin' - end + around do |example| + with_resources_during(example) { ActiveAdmin.register Category } + end - it "should return the route collection path" do - expect(config.route_collection_path).to eq '/admin/categories' - end + let(:category) { Category.new { |c| c.id = 123 } } - it "should return the route instance path" do - expect(config.route_instance_path(category)).to eq '/admin/categories/123' - end - end + it "should return the route prefix" do + expect(config.route_prefix).to eq "admin" + end - context "when in the root namespace" do - let!(:config) { ActiveAdmin.register Category, namespace: false } - it "should have a nil route_prefix" do - expect(config.route_prefix).to be_nil - end + it "should return the route collection path" do + expect(config.route_collection_path).to eq "/admin/categories" + end - it "should generate a correct route" do - reload_routes! - expect(config.route_collection_path).to eq "/categories" - end + it "should return the route instance path" do + expect(config.route_instance_path(category)).to eq "/admin/categories/123" + end + end + + context "when in the root namespace" do + let!(:config) { ActiveAdmin.register Category, namespace: false } + + around do |example| + with_resources_during(example) { config } + end + + after do + application.namespace(:root).unload! + application.namespaces.instance_variable_get(:@namespaces).delete(:root) + end + + it "should have a nil route_prefix" do + expect(config.route_prefix).to eq nil + end + + it "should generate a correct route" do + expect(config.route_collection_path).to eq "/categories" + end + end + + context "when registering a plural resource" do + class ::News; def self.has_many(*); end end + let!(:config) { ActiveAdmin.register News } + + around do |example| + with_resources_during(example) { config } + end + + it "should return the plural route with _index" do + expect(config.route_collection_path).to eq "/admin/news" + end + end + + context "when the resource belongs to another resource" do + let(:config) { namespace.resource_for("Post") } + + let :post do + Post.new do |p| + p.id = 3 + p.category = Category.new { |c| c.id = 1 } end + end - context "when registering a plural resource" do - class ::News; def self.has_many(*); end end - let!(:config) { ActiveAdmin.register News } - before{ reload_routes! } + before do + load_resources do + ActiveAdmin.register Category + ActiveAdmin.register(Post) do + belongs_to :category - it "should return the plural route with _index" do - expect(config.route_collection_path).to eq "/admin/news" + member_action :foo end end + end - context "when the resource belongs to another resource" do - let! :config do - ActiveAdmin.register Post do - belongs_to :category - end - end + it "should nest the collection path" do + expect(config.route_collection_path(category_id: 1)).to eq "/admin/categories/1/posts" + end - let :post do - Post.new do |p| - p.id = 3 - p.category = Category.new{ |c| c.id = 1 } - end - end + it "should nest the instance path" do + expect(config.route_instance_path(post)).to eq "/admin/categories/1/posts/3" + end - before{ reload_routes! } + it "should nest the member action path" do + expect(config.route_member_action_path(:foo, post)).to eq "/admin/categories/1/posts/3/foo" + end + end - it "should nest the collection path" do - expect(config.route_collection_path(category_id: 1)).to eq "/admin/categories/1/posts" - end + context "for batch_action handler" do + before do + load_resources { config.batch_actions = true } + end - it "should nest the instance path" do - expect(config.route_instance_path(post)).to eq "/admin/categories/1/posts/3" + context "when register a singular resource" do + let :config do + ActiveAdmin.register Category + ActiveAdmin.register Post do + belongs_to :category end end + + it "should include :scope and :q params" do + params = ActionController::Parameters.new(category_id: 1, q: { name_eq: "Any" }, scope: :all) + additional_params = { locale: "en" } + batch_action_path = "/admin/categories/1/posts/batch_action?locale=en&q%5Bname_eq%5D=Any&scope=all" + + expect(config.route_batch_action_path(params, additional_params)).to eq batch_action_path + end + end + + context "when registering a plural resource" do + class ::News; def self.has_many(*); end end + let(:config) { ActiveAdmin.register News } + + it "should return the plural batch action route with _index and given params" do + params = ActionController::Parameters.new(q: { name_eq: "Any" }, scope: :all) + additional_params = { locale: "en" } + batch_action_path = "/admin/news/batch_action?locale=en&q%5Bname_eq%5D=Any&scope=all" + expect(config.route_batch_action_path(params, additional_params)).to eq batch_action_path + end end end end diff --git a/spec/unit/resource/scopes_spec.rb b/spec/unit/resource/scopes_spec.rb index 3e132699f48..78a94771abb 100644 --- a/spec/unit/resource/scopes_spec.rb +++ b/spec/unit/resource/scopes_spec.rb @@ -1,26 +1,23 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" module ActiveAdmin - describe Resource, "Scopes" do - - before { load_defaults! } - - let(:application){ ActiveAdmin::Application.new } - let(:namespace){ Namespace.new(application, :admin) } + RSpec.describe Resource, "Scopes" do + let(:application) { ActiveAdmin::Application.new } + let(:namespace) { Namespace.new(application, :admin) } def config(options = {}) @config ||= Resource.new(namespace, Category, options) end describe "adding a scope" do - it "should add a scope" do config.scope :published expect(config.scopes.first).to be_a(ActiveAdmin::Scope) expect(config.scopes.first.name).to eq "Published" end - it "should retrive a scope by its id" do + it "should retrieve a scope by its id" do config.scope :published expect(config.get_scope_by_id(:published).name).to eq "Published" end @@ -40,11 +37,10 @@ def config(options = {}) it "should update a scope with the same id" do config.scope :published - expect(config.scopes.first.scope_block).to be_nil - config.scope(:published){ } - expect(config.scopes.first.scope_block).to_not be_nil + expect(config.scopes.first.scope_block).to eq nil + config.scope(:published) {} + expect(config.scopes.first.scope_block).to_not eq nil end - end end end diff --git a/spec/unit/resource/sidebars_spec.rb b/spec/unit/resource/sidebars_spec.rb index 94289631653..78603a065f7 100644 --- a/spec/unit/resource/sidebars_spec.rb +++ b/spec/unit/resource/sidebars_spec.rb @@ -1,7 +1,7 @@ -require 'rails_helper' - -describe ActiveAdmin::Resource::Sidebars do +# frozen_string_literal: true +require "rails_helper" +RSpec.describe ActiveAdmin::Resource::Sidebars do let(:resource) do namespace = ActiveAdmin::Namespace.new(ActiveAdmin::Application.new, :admin) namespace.register(Post) @@ -10,7 +10,6 @@ let(:sidebar) { ActiveAdmin::SidebarSection.new(:help) } describe "adding a new sidebar section" do - before do resource.clear_sidebar_sections! resource.sidebar_sections << sidebar @@ -19,13 +18,11 @@ it "should add a sidebar section" do expect(resource.sidebar_sections.size).to eq(1) end - end describe "retrieving sections for a controller action" do - - let(:only_index){ ActiveAdmin::SidebarSection.new(:help, only: :index) } - let(:only_show){ ActiveAdmin::SidebarSection.new(:help, only: :show) } + let(:only_index) { ActiveAdmin::SidebarSection.new(:help, only: :index) } + let(:only_show) { ActiveAdmin::SidebarSection.new(:help, only: :show) } before do resource.clear_sidebar_sections! @@ -37,7 +34,5 @@ expect(resource.sidebar_sections.size).to eq(2) expect(resource.sidebar_sections_for("index")).to eq [only_index] end - end - end diff --git a/spec/unit/resource_collection_spec.rb b/spec/unit/resource_collection_spec.rb index 56881f06754..2952b3bead4 100644 --- a/spec/unit/resource_collection_spec.rb +++ b/spec/unit/resource_collection_spec.rb @@ -1,20 +1,21 @@ -require 'rails_helper' -require 'active_admin/resource_collection' +# frozen_string_literal: true +require "rails_helper" +require "active_admin/resource_collection" -describe ActiveAdmin::ResourceCollection do +RSpec.describe ActiveAdmin::ResourceCollection do let(:application) { ActiveAdmin::Application.new } - let(:namespace) { ActiveAdmin::Namespace.new application, :admin } - let(:collection) { ActiveAdmin::ResourceCollection.new } - let(:resource) { double resource_name: "MyResource" } + let(:namespace) { ActiveAdmin::Namespace.new application, :admin } + let(:collection) { ActiveAdmin::ResourceCollection.new } + let(:resource) { double resource_name: "MyResource" } - it { is_expected.to respond_to :[] } - it { is_expected.to respond_to :add } - it { is_expected.to respond_to :each } + it { is_expected.to respond_to :[] } + it { is_expected.to respond_to :add } + it { is_expected.to respond_to :each } it { is_expected.to respond_to :has_key? } - it { is_expected.to respond_to :keys } - it { is_expected.to respond_to :values } - it { is_expected.to respond_to :size } - it { is_expected.to respond_to :to_a } + it { is_expected.to respond_to :keys } + it { is_expected.to respond_to :values } + it { is_expected.to respond_to :size } + it { is_expected.to respond_to :to_a } it "should have no resources when new" do expect(collection).to be_empty @@ -22,7 +23,7 @@ it "should be enumerable" do collection.add(resource) - collection.each{ |r| expect(r).to eq resource } + collection.each { |r| expect(r).to eq resource } end it "should return the available keys" do @@ -51,22 +52,22 @@ end it "shouldn't allow a resource name mismatch to occur" do - expect { + expect do ActiveAdmin.register Category ActiveAdmin.register Post, as: "Category" - }.to raise_error ActiveAdmin::ResourceCollection::ConfigMismatch + end.to raise_error ActiveAdmin::ResourceCollection::ConfigMismatch end it "shouldn't allow a Page/Resource mismatch to occur" do - expect { + expect do ActiveAdmin.register User - ActiveAdmin.register_page 'User' - }.to raise_error ActiveAdmin::ResourceCollection::IncorrectClass + ActiveAdmin.register_page "User" + end.to raise_error ActiveAdmin::ResourceCollection::IncorrectClass end describe "should store both renamed and non-renamed resources" do let(:resource) { ActiveAdmin::Resource.new namespace, Category } - let(:renamed) { ActiveAdmin::Resource.new namespace, Category, as: "Subcategory" } + let(:renamed) { ActiveAdmin::Resource.new namespace, Category, as: "Subcategory" } it "when the renamed version is added first" do collection.add renamed @@ -83,12 +84,12 @@ end describe "#[]" do - let(:resource) { ActiveAdmin::Resource.new namespace, resource_class } - let(:inherited_resource) { ActiveAdmin::Resource.new namespace, inherited_resource_class } + let(:resource) { ActiveAdmin::Resource.new namespace, resource_class } + let(:inherited_resource) { ActiveAdmin::Resource.new namespace, inherited_resource_class } - let(:resource_class) { User } + let(:resource_class) { User } let(:inherited_resource_class) { Publisher } - let(:unregistered_class) { Category } + let(:unregistered_class) { Category } context "with resources" do before do @@ -133,7 +134,7 @@ context "with a renamed resource" do let(:renamed_resource) { ActiveAdmin::Resource.new namespace, resource_class, as: name } - let(:name) { "Administrators" } + let(:name) { "Administrators" } before do collection.add renamed_resource @@ -169,7 +170,4 @@ end end end - - skip "specs for subclasses of Page and Resource" - end diff --git a/spec/unit/resource_controller/data_access_spec.rb b/spec/unit/resource_controller/data_access_spec.rb index e0de719f1ee..5a4feeb4c15 100644 --- a/spec/unit/resource_controller/data_access_spec.rb +++ b/spec/unit/resource_controller/data_access_spec.rb @@ -1,10 +1,26 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::ResourceController::DataAccess do - let(:params) do +RSpec.describe ActiveAdmin::ResourceController::DataAccess do + around do |example| + with_resources_during(example) do + config + end + end + + let(:config) do + ActiveAdmin.register Post do + end + end + + let(:http_params) do {} end + let(:params) do + ActionController::Parameters.new(http_params) + end + let(:controller) do rc = Admin::PostsController.new allow(rc).to receive(:params) do @@ -14,28 +30,42 @@ end describe "searching" do - let(:params) {{ q: {} }} + let(:ransack_options) { { auth_object: controller.send(:active_admin_authorization) } } + let(:http_params) { { q: {} } } it "should call the search method" do chain = double "ChainObj" - expect(chain).to receive(:ransack).with(params[:q]).once.and_return(Post.ransack) + expect(chain).to receive(:ransack).with(params[:q], ransack_options).once.and_return(Post.ransack) controller.send :apply_filtering, chain end + + context "params includes empty values" do + let(:http_params) do + { q: { id_eq: 1, position_eq: "" } } + end + it "should return relation without empty filters" do + expect(Post).to receive(:ransack).with(params[:q], ransack_options).once.and_wrap_original do |original, *args| + chain = original.call(*args) + expect(chain.conditions.size).to eq(1) + chain + end + controller.send :apply_filtering, Post + end + end end describe "sorting" do - context "valid clause" do - let(:params) {{ order: "id_asc" }} + let(:http_params) { { order: "id_asc" } } it "reorders chain" do chain = double "ChainObj" - expect(chain).to receive(:reorder).with('"posts"."id" asc').once.and_return(Post.search) + expect(chain).to receive(:reorder).with('"posts"."id" asc').once.and_return(Post.ransack) controller.send :apply_sorting, chain end end context "invalid clause" do - let(:params) {{ order: "_asc" }} + let(:http_params) { { order: "_asc" } } it "returns chain untouched" do chain = double "ChainObj" @@ -44,10 +74,38 @@ end end + context "custom strategy" do + before do + expect(controller.send(:active_admin_config)).to receive(:ordering).twice.and_return( + { + published_date: proc do |order_clause| + [order_clause.to_sql, "NULLS LAST"].join(" ") if order_clause.order == "desc" + end + }.with_indifferent_access + ) + end + + context "when params applicable" do + let(:http_params) { { order: "published_date_desc" } } + it "reorders chain" do + chain = double "ChainObj" + expect(chain).to receive(:reorder).with('"posts"."published_date" desc NULLS LAST').once.and_return(Post.ransack) + controller.send :apply_sorting, chain + end + end + + context "when params not applicable" do + let(:http_params) { { order: "published_date_asc" } } + it "reorders chain" do + chain = double "ChainObj" + expect(chain).to receive(:reorder).with('"posts"."published_date" asc').once.and_return(Post.ransack) + controller.send :apply_sorting, chain + end + end + end end describe "scoping" do - context "when no current scope" do it "should set collection_before_scope to the chain and return the chain" do chain = double "ChainObj" @@ -89,6 +147,39 @@ end end + describe "pagination" do + let(:collection) do + Post.create! + Post.all + end + + let(:config) do + ActiveAdmin.register Post do + config.per_page = 1 + end + end + + context "when CSV format requested" do + let(:params) do + ActionController::Parameters.new(page: 2, format: "csv") + end + + it "does not apply it" do + expect(controller.send(:apply_pagination, collection).size).to eq(1) + end + end + + context "when CSV format not requested" do + let(:params) do + ActionController::Parameters.new(page: 2) + end + + it "applies it" do + expect(controller.send(:apply_pagination, collection).size).to eq(0) + end + end + end + describe "find_collection" do let(:appliers) do ActiveAdmin::ResourceController::DataAccess::COLLECTION_APPLIES @@ -137,4 +228,62 @@ end end end + + describe "build_resource" do + let(:config) do + ActiveAdmin.register User do + permit_params :type, posts_attributes: :custom_category_id + end + end + + let!(:category) { Category.create!(name: "Category") } + + let(:params) do + ActionController::Parameters.new(user: { type: "User::VIP", posts_attributes: [custom_category_id: category.id] }) + end + + subject do + controller.send :build_resource + end + + let(:controller) do + rc = Admin::UsersController.new + allow(rc).to receive(:params) do + params + end + rc + end + + it "should return post with assigned attributes" do + expect(subject).to be_a(User::VIP) + end + + # see issue 4548 + it "should assign nested attributes once" do + expect(subject.posts.size).to eq(1) + end + + context "given authorization scope" do + let(:authorization) { controller.send(:active_admin_authorization) } + + it "should apply authorization scope" do + expect(authorization).to receive(:scope_collection) do |collection| + collection.where(age: "42") + end + expect(subject.age).to eq(42) + end + end + end + + describe "in_paginated_batches" do + it "calls find_collection just once and disables the ActiveRecord query cache" do + expect(controller).to receive(:find_collection).once do + expect(ActiveRecord::Base.connection.query_cache_enabled).to be_falsy + Post.none + end + ActiveRecord::Base.cache do + controller.send(:in_paginated_batches, &Proc.new {}) + end + end + end end diff --git a/spec/unit/resource_controller/decorators_spec.rb b/spec/unit/resource_controller/decorators_spec.rb index 7edf6386005..831c300f861 100644 --- a/spec/unit/resource_controller/decorators_spec.rb +++ b/spec/unit/resource_controller/decorators_spec.rb @@ -1,83 +1,95 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::ResourceController::Decorators do - let(:controller_class) do - Class.new do - def self.name - "Test Controller using Decorators" - end +RSpec.describe ActiveAdmin::ResourceController::Decorators do + describe "#apply_decorator" do + let(:resource) { Post.new } + let(:controller) { controller_with_decorator(action, decorator_class) } + subject(:applied) { controller.apply_decorator(resource) } - include ActiveAdmin::ResourceController::Decorators + context "in show action" do + let(:action) { "show" } - public :apply_decorator, :apply_collection_decorator - end - end + context "with a Draper decorator" do + let(:decorator_class) { PostDecorator } - let(:controller) { controller_class.new } - let(:active_admin_config) { double(decorator_class: decorator_class) } - before do - allow(controller).to receive(:active_admin_config).and_return(active_admin_config) - allow(controller).to receive(:action_name).and_return(action) - end + it { is_expected.to be_kind_of(PostDecorator) } + end - describe '#apply_decorator' do - let(:action) { 'show' } - let(:resource) { Post.new } - subject(:applied) { controller.apply_decorator(resource) } + context "with a PORO decorator" do + let(:decorator_class) { PostPoroDecorator } - context 'with a decorator configured' do - let(:decorator_class) { PostDecorator } - it { is_expected.to be_kind_of(PostDecorator) } + it { is_expected.to be_kind_of(PostPoroDecorator) } + end end - context 'with no decorator configured' do + context "in update action" do + let(:action) { "update" } let(:decorator_class) { nil } - it { is_expected.to be_kind_of(Post) } + + it { is_expected.not_to be_kind_of(PostDecorator) } end end - describe '#apply_collection_decorator' do + describe "#apply_collection_decorator" do before { Post.create! } - let(:action) { 'index' } let(:collection) { Post.where nil } - subject(:applied) { controller.apply_collection_decorator(collection) } - context 'when a decorator is configured' do - context 'and it is using a recent version of draper' do - let(:decorator_class) { PostDecorator } - - it 'calling certain scope collections work' do - # This is an example of one of the methods that was consistently - # failing before this feature existed - expect(applied.reorder('').count).to eq applied.count + context "with a Draper decorator" do + let(:controller) { controller_with_decorator("index", PostDecorator) } + subject(:applied) { controller.apply_collection_decorator(collection) } + + context "when a decorator is configured" do + context "and it is using a recent version of draper" do + it "calling certain scope collections work" do + # This is an example of one of the methods that was consistently + # failing before this feature existed + expect(applied.reorder("").count).to eq applied.count + end + + it "has a good description for the generated class" do + expect(applied.class.name).to eq "Draper::CollectionDecorator of PostDecorator + ActiveAdmin" + end end + end + end - it 'has a good description for the generated class' do - expect(applied.class.name).to eq "Draper::CollectionDecorator of PostDecorator + ActiveAdmin" - end + context "with a PORO decorator" do + let(:controller) { controller_with_decorator("index", PostPoroDecorator) } + subject(:applied) { controller.apply_collection_decorator(collection) } + + it "returns a presented collection" do + expect(subject).to be_kind_of(ActiveAdmin::CollectionDecorator) + expect(subject).to all(be_a(PostPoroDecorator)) + end + it "has a good description for the generated class" do + expect(applied.class.name).to eq "ActiveAdmin::CollectionDecorator of PostPoroDecorator + ActiveAdmin" end end end - describe 'form actions' do - let(:action) { 'edit' } + describe "form actions" do let(:resource) { Post.new } - let(:form_presenter) { double options: { decorate: decorate_form } } - let(:decorator_class) { PostDecorator } - before { allow(active_admin_config).to receive(:get_page_presenter).with(:form).and_return form_presenter } + let(:controller) { controller_with_decorator("edit", decorator_class) } subject(:applied) { controller.apply_decorator(resource) } - context 'when the form is not configured to decorate' do - let(:decorate_form) { false } + context "when the form is not configured to decorate" do + let(:decorator_class) { nil } it { is_expected.to be_kind_of(Post) } end - context 'when the form is configured to decorate' do - let(:decorate_form) { true } - it { is_expected.to be_kind_of(PostDecorator) } - end + context "when the form is configured to decorate" do + context "with a Draper decorator" do + let(:decorator_class) { PostDecorator } + it { is_expected.to be_kind_of(PostDecorator) } + end + context "with a PORO decorator" do + let(:decorator_class) { PostPoroDecorator } + it { is_expected.to be_kind_of(PostPoroDecorator) } + end + end end end diff --git a/spec/unit/resource_controller/polymorphic_routes_spec.rb b/spec/unit/resource_controller/polymorphic_routes_spec.rb new file mode 100644 index 00000000000..549714d2eb3 --- /dev/null +++ b/spec/unit/resource_controller/polymorphic_routes_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::ResourceController::PolymorphicRoutes, type: :controller do + let(:klass) { Admin::PostsController } + + %w(polymorphic_url polymorphic_path).each do |method| + describe method do + let(:add_extra_routes) {} + let(:params) { {} } + + before do + load_resources { post_config } + + add_extra_routes + + @controller = klass.new + + get :index, params: params + end + + context "without belongs_to" do + let(:post_config) { ActiveAdmin.register Post } + let(:post) { Post.create! title: "Hello World" } + + it "works with no parent" do + expect(controller.send(method, [:admin, post])).to include("/admin/posts/#{post.id}") + end + end + + context "with belongs_to" do + let(:user) { User.create! } + let(:post) { Post.create! title: "Hello World", author: user } + + let(:post_config) do + ActiveAdmin.register User + ActiveAdmin.register Post do + belongs_to :user, optional: true + end + end + + %w(posts user_posts).each do |current_page| + context "within the #{current_page} page" do + let(:filter_param) { current_page.sub(/_?posts/, "").presence } + let(:params) { filter_param ? { "#{filter_param}_id" => send(filter_param).id } : {} } + + it "works with no parent" do + expect(controller.send(method, [:admin, post])).to include("/admin/posts/#{post.id}") + end + + it "works with a user as parent" do + expect(controller.send(method, [:admin, user, post])).to include("/admin/users/#{user.id}/posts/#{post.id}") + end + end + end + end + + context "with multiple belongs_to (not fully supported yet)" do + let(:user) { User.create! } + let(:category) { Category.create! name: "Category" } + let(:post) { Post.create! title: "Hello World", author: user, category: category } + + let(:post_config) do + ActiveAdmin.register User + ActiveAdmin.register Category + ActiveAdmin.register Post do + belongs_to :category, optional: true + belongs_to :user, optional: true + end + end + + let(:add_extra_routes) do + routes.draw do + ActiveAdmin.routes(self) + namespace :admin do + resources :posts + resources :users do + resources :posts + end + resources :categories do + resources :posts + end + end + end + end + + %w(posts category_posts user_posts).each do |current_page| + context "within the #{current_page} page" do + let(:filter_param) { current_page.sub(/_?posts/, "").presence } + let(:params) { filter_param ? { "#{filter_param}_id" => send(filter_param).id } : {} } + + it "works with no parent" do + expect(controller.send(method, [:admin, post])).to include("/admin/posts/#{post.id}") + end + + it "works with a user as parent" do + expect(controller.send(method, [:admin, user, post])).to include("/admin/users/#{user.id}/posts/#{post.id}") + end + + it "works with a category as parent" do + expect(controller.send(method, [:admin, category, post])).to include("/admin/categories/#{category.id}/posts/#{post.id}") + end + end + end + end + end + end +end diff --git a/spec/unit/resource_controller/sidebars_spec.rb b/spec/unit/resource_controller/sidebars_spec.rb deleted file mode 100644 index d5f362ffe2c..00000000000 --- a/spec/unit/resource_controller/sidebars_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::ResourceController::Sidebars do - let(:controller){ Admin::PostsController } - - context 'without before_filter' do - before do - ActiveAdmin.register Post - end - - subject { find_before_filter controller, :skip_sidebar! } - - it { is_expected.to set_skip_sidebar_to nil, for: controller } - end - - describe '#skip_sidebar!' do - before do - ActiveAdmin.register Post do - before_filter :skip_sidebar! - end - end - - subject { find_before_filter controller, :skip_sidebar! } - - it { is_expected.to set_skip_sidebar_to true, for: controller } - end - - def find_before_filter(controller, filter) - #raise controller._process_action_callbacks.map(&:filter).inspect - controller._process_action_callbacks.detect { |f| f.raw_filter == filter.to_sym } - end - - RSpec::Matchers.define :set_skip_sidebar_to do |expected, options| - match do |filter| - object = options[:for].new - object.send filter.raw_filter if filter - @actual = object.instance_variable_get(:@skip_sidebar) - expect(@actual).to eq expected - end - - failure_message do |filter| - message = "expected before_filter to set @skip_sidebar to '#{expected}', but was '#{@actual}'" - end - end -end diff --git a/spec/unit/resource_controller_spec.rb b/spec/unit/resource_controller_spec.rb index 71cabeab813..d9f9db924e9 100644 --- a/spec/unit/resource_controller_spec.rb +++ b/spec/unit/resource_controller_spec.rb @@ -1,88 +1,42 @@ -require 'rails_helper' - -describe ActiveAdmin::ResourceController do - - let(:controller) { ActiveAdmin::ResourceController.new } - - describe "authenticating the user" do - let(:controller){ Admin::PostsController.new } - - it "should do nothing when no authentication_method set" do - namespace = controller.class.active_admin_config.namespace - expect(namespace).to receive(:authentication_method).once.and_return(nil) - - controller.send(:authenticate_active_admin_user) - end - - it "should call the authentication_method when set" do - namespace = controller.class.active_admin_config.namespace - - expect(namespace).to receive(:authentication_method).twice. - and_return(:authenticate_admin_user!) - - expect(controller).to receive(:authenticate_admin_user!).and_return(true) - - controller.send(:authenticate_active_admin_user) - end - - end - - describe "retrieving the current user" do - let(:controller){ Admin::PostsController.new } - - it "should return nil when no current_user_method set" do - namespace = controller.class.active_admin_config.namespace - expect(namespace).to receive(:current_user_method).once.and_return(nil) - - expect(controller.send(:current_active_admin_user)).to eq nil - end - - it "should call the current_user_method when set" do - user = double - namespace = controller.class.active_admin_config.namespace - - expect(namespace).to receive(:current_user_method).twice. - and_return(:current_admin_user) - - expect(controller).to receive(:current_admin_user).and_return(user) - - expect(controller.send(:current_active_admin_user)).to eq user - end - end +# frozen_string_literal: true +require "rails_helper" +RSpec.describe ActiveAdmin::ResourceController, type: :controller do + let(:controller) { Admin::PostsController.new } describe "callbacks" do - before :all do - application = ::ActiveAdmin::Application.new - namespace = ActiveAdmin::Namespace.new(application, :admin) - namespace.register Post do - after_build :call_after_build - before_save :call_before_save - after_save :call_after_save - before_create :call_before_create - after_create :call_after_create - before_update :call_before_update - after_update :call_after_update - before_destroy :call_before_destroy - after_destroy :call_after_destroy - - controller do - def call_after_build(obj); end - def call_before_save(obj); end - def call_after_save(obj); end - def call_before_create(obj); end - def call_after_create(obj); end - def call_before_update(obj); end - def call_after_update(obj); end - def call_before_destroy(obj); end - def call_after_destroy(obj); end + around do |example| + with_resources_during(example) do + ActiveAdmin.register(Post) do + after_build :call_after_build + before_save :call_before_save + after_save :call_after_save + before_create :call_before_create + after_create :call_after_create + before_update :call_before_update + after_update :call_after_update + before_destroy :call_before_destroy + after_destroy :call_after_destroy + + controller do + private + + def call_after_build(obj); end + def call_before_save(obj); end + def call_after_save(obj); end + def call_before_create(obj); end + def call_after_create(obj); end + def call_before_update(obj); end + def call_after_update(obj); end + def call_before_destroy(obj); end + def call_after_destroy(obj); end + end end end end describe "performing create" do - let(:controller){ Admin::PostsController.new } - let(:resource){ double("Resource", save: true) } + let(:resource) { double("Resource", save: true) } before do expect(resource).to receive(:save) @@ -92,14 +46,17 @@ def call_after_destroy(obj); end expect(controller).to receive(:call_before_create).with(resource) controller.send :create_resource, resource end + it "should call the before save callback" do expect(controller).to receive(:call_before_save).with(resource) controller.send :create_resource, resource end + it "should call the after save callback" do expect(controller).to receive(:call_after_save).with(resource) controller.send :create_resource, resource end + it "should call the after create callback" do expect(controller).to receive(:call_after_create).with(resource) controller.send :create_resource, resource @@ -107,9 +64,8 @@ def call_after_destroy(obj); end end describe "performing update" do - let(:controller){ Admin::PostsController.new } - let(:resource){ double("Resource", :attributes= => true, save: true) } - let(:attributes){ [{}] } + let(:resource) { double("Resource", :attributes= => true, save: true) } + let(:attributes) { [{}] } before do expect(resource).to receive(:attributes=).with(attributes[0]) @@ -120,14 +76,17 @@ def call_after_destroy(obj); end expect(controller).to receive(:call_before_update).with(resource) controller.send :update_resource, resource, attributes end + it "should call the before save callback" do expect(controller).to receive(:call_before_save).with(resource) controller.send :update_resource, resource, attributes end + it "should call the after save callback" do expect(controller).to receive(:call_after_save).with(resource) controller.send :update_resource, resource, attributes end + it "should call the after create callback" do expect(controller).to receive(:call_after_update).with(resource) controller.send :update_resource, resource, attributes @@ -135,8 +94,7 @@ def call_after_destroy(obj); end end describe "performing destroy" do - let(:controller){ Admin::PostsController.new } - let(:resource){ double("Resource", destroy: true) } + let(:resource) { double("Resource", destroy: true) } before do expect(resource).to receive(:destroy) @@ -153,18 +111,96 @@ def call_after_destroy(obj); end end end end + + describe "action methods" do + around do |example| + with_resources_during(example) do + load_resources { ActiveAdmin.register Post } + end + end + + it "should have actual action methods" do + controller.class.clear_action_methods! # make controller recalculate :action_methods on the next call + expect(controller.action_methods.sort).to eq ["batch_action", "create", "destroy", "edit", "index", "new", "show", "update"] + end + end + + describe "resource update" do + let(:controller) { Admin::CompaniesController.new } + + around do |example| + with_resources_during(example) do + ActiveAdmin.register Company + end + end + + it "should not update habtm associations when the resource validation fails" do + resource = Company.create! name: "my company", stores: [Store.create!(name: "store 1")] + + controller.send(:update_resource, resource, [{ name: "", store_ids: [] }]) + + expect(resource.reload.stores).not_to be_empty + end + end end -describe Admin::PostsController, type: "controller" do +RSpec.describe "A specific resource controller", type: :controller do + around do |example| + with_resources_during(example) do + ActiveAdmin.register Post + @controller = Admin::PostsController.new + end + end + + describe "authenticating the user" do + it "should do nothing when no authentication_method set" do + namespace = controller.class.active_admin_config.namespace + expect(namespace).to receive(:authentication_method).once.and_return(nil) + + controller.send(:authenticate_active_admin_user) + end + + it "should call the authentication_method when set" do + namespace = controller.class.active_admin_config.namespace + + expect(namespace).to receive(:authentication_method).twice. + and_return(:authenticate_admin_user!) + + expect(controller).to receive(:authenticate_admin_user!).and_return(true) + + controller.send(:authenticate_active_admin_user) + end + end + + describe "retrieving the current user" do + it "should return nil when no current_user_method set" do + namespace = controller.class.active_admin_config.namespace + expect(namespace).to receive(:current_user_method).once.and_return(nil) + + expect(controller.send(:current_active_admin_user)).to eq nil + end + + it "should call the current_user_method when set" do + user = double + namespace = controller.class.active_admin_config.namespace + + expect(namespace).to receive(:current_user_method).twice. + and_return(:current_admin_user) - describe 'retrieving the resource' do - let(:controller){ Admin::PostsController.new } + expect(controller).to receive(:current_admin_user).and_return(user) + + expect(controller.send(:current_active_admin_user)).to eq user + end + end + + describe "retrieving the resource" do let(:post) { Post.new title: "An incledibly unique Post Title" } + let(:http_params) { { id: "1" } } before do allow(Post).to receive(:find).and_return(post) controller.class_eval { public :resource } - allow(controller).to receive(:params).and_return({ id: '1' }) + allow(controller).to receive(:params).and_return(ActionController::Parameters.new(http_params)) end subject { controller.resource } @@ -173,28 +209,43 @@ def call_after_destroy(obj); end expect(subject).to be_kind_of(Post) end - context 'with a decorator' do + context "with a decorator" do let(:config) { controller.class.active_admin_config } - before { config.decorator_class_name = '::PostDecorator' } - it 'returns a PostDecorator' do - expect(subject).to be_kind_of(PostDecorator) + + context "with a Draper decorator" do + before { config.decorator_class_name = "::PostDecorator" } + + it "returns a PostDecorator" do + expect(subject).to be_kind_of(PostDecorator) + end + + it "returns a PostDecorator that wraps the post" do + expect(subject.title).to eq post.title + end end - it 'returns a PostDecorator that wraps the post' do - expect(subject.title).to eq post.title + context "with a PORO decorator" do + before { config.decorator_class_name = "::PostPoroDecorator" } + + it "returns a PostDecorator" do + expect(subject).to be_kind_of(PostPoroDecorator) + end + + it "returns a PostDecorator that wraps the post" do + expect(subject.title).to eq post.title + end end end end - describe 'retrieving the resource collection' do - let(:controller){ Admin::PostsController.new } + describe "retrieving the resource collection" do let(:config) { controller.class.active_admin_config } before do - Post.create!(title: "An incledibly unique Post Title") if Post.count == 0 + Post.create!(title: "An incledibly unique Post Title") config.decorator_class_name = nil - request = double 'Request', format: 'application/json' + request = double "Request", format: "application/json" allow(controller).to receive(:params) { {} } - allow(controller).to receive(:request){ request } + allow(controller).to receive(:request) { request } end subject { controller.send :collection } @@ -207,32 +258,35 @@ def call_after_destroy(obj); end expect(subject.first).to be_kind_of(Post) end - context 'with a decorator' do - before { config.decorator_class_name = 'PostDecorator' } + context "with a decorator" do + before { config.decorator_class_name = "PostDecorator" } - it 'returns a collection decorator using PostDecorator' do + it "returns a collection decorator using PostDecorator" do expect(subject).to be_a Draper::CollectionDecorator expect(subject.decorator_class).to eq PostDecorator end - it 'returns a collection decorator that wraps the post' do + it "returns a collection decorator that wraps the post" do expect(subject.first.title).to eq Post.first.title end end end - describe "performing batch_action" do - let(:controller){ Admin::PostsController.new } - let(:batch_action) { ActiveAdmin::BatchAction.new :flag, "Flag", &batch_action_block } - let(:batch_action_block) { proc { } } + let(:batch_action) { ActiveAdmin::BatchAction.new(*batch_action_args, &batch_action_block) } + let(:batch_action_block) { proc { self.instance_variable_set :@block_context, self.class } } + let(:params) { ActionController::Parameters.new(http_params) } + before do allow(controller.class.active_admin_config).to receive(:batch_actions).and_return([batch_action]) + allow(controller).to receive(:params) { params } end describe "when params batch_action matches existing BatchAction" do - before do - allow(controller).to receive(:params) { { batch_action: "flag", collection_selection: ["1"] } } + let(:batch_action_args) { [:flag, "Flag"] } + + let(:http_params) do + { batch_action: "flag", collection_selection: ["1"] } end it "should call the block with args" do @@ -241,29 +295,50 @@ def call_after_destroy(obj); end end it "should call the block in controller scope" do - expect(controller).to receive(:render_in_context).with(controller, nil).and_return({}) + controller.batch_action + expect(controller.instance_variable_get(:@block_context)).to eq Admin::PostsController + end + end + + describe "when params batch_action matches existing BatchAction and form inputs defined" do + let(:batch_action_args) { [:flag, "Flag"] } + + let(:http_params) do + { batch_action: "flag", collection_selection: ["1"], batch_action_inputs: '{ "a": "1", "b": "2" }' } + end + + it "should include params" do + expect(controller).to receive(:instance_exec).with(["1"], { "a" => "1", "b" => "2" }) controller.batch_action end end describe "when params batch_action doesn't match a BatchAction" do + let(:batch_action_args) { [:flag, "Flag"] } + + let(:http_params) do + { batch_action: "derp", collection_selection: ["1"] } + end + it "should raise an error" do - allow(controller).to receive(:params) { { batch_action: "derp", collection_selection: ["1"] } } - expect { + expect do controller.batch_action - }.to raise_error("Couldn't find batch action \"derp\"") + end.to raise_error("Couldn't find batch action \"derp\"") end end describe "when params batch_action is blank" do + let(:batch_action_args) { [:flag, "Flag"] } + + let(:http_params) do + { collection_selection: ["1"] } + end + it "should raise an error" do - allow(controller).to receive(:params) { { collection_selection: ["1"] } } - expect { + expect do controller.batch_action - }.to raise_error("Couldn't find batch action \"\"") + end.to raise_error("Couldn't find batch action \"\"") end end - end - end diff --git a/spec/unit/resource_registration_spec.rb b/spec/unit/resource_registration_spec.rb index 43022fedf12..6120d5717f8 100644 --- a/spec/unit/resource_registration_spec.rb +++ b/spec/unit/resource_registration_spec.rb @@ -1,19 +1,31 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe "Registering an object to administer" do - application = ActiveAdmin::Application.new +RSpec.describe "Registering an object to administer" do + let(:application) { ActiveAdmin::Application.new } context "with no configuration" do - namespace = ActiveAdmin::Namespace.new(application, :admin) - it "should call register on the namespace" do + let(:namespace) { ActiveAdmin::Namespace.new(application, :admin) } + + before do application.namespaces[namespace.name] = namespace + end + + it "should call register on the namespace" do expect(namespace).to receive(:register) application.register Category end it "should dispatch a Resource::RegisterEvent" do - expect(ActiveSupport::Notifications).to receive(:publish).with(ActiveAdmin::Resource::RegisterEvent, an_instance_of(ActiveAdmin::Resource)) + expect(ActiveSupport::Notifications).to( + receive(:instrument) + .with( + ActiveAdmin::Resource::RegisterEvent, + hash_including(active_admin_resource: an_instance_of(ActiveAdmin::Resource)) + ) + ) + application.register Category end end @@ -28,8 +40,20 @@ end it "should generate a Namespace::RegisterEvent and a Resource::RegisterEvent" do - expect(ActiveSupport::Notifications).to receive(:publish).with(ActiveAdmin::Namespace::RegisterEvent, an_instance_of(ActiveAdmin::Namespace)) - expect(ActiveSupport::Notifications).to receive(:publish).with(ActiveAdmin::Resource::RegisterEvent, an_instance_of(ActiveAdmin::Resource)) + expect(ActiveSupport::Notifications).to( + receive(:instrument) + .with( + ActiveAdmin::Namespace::RegisterEvent, + hash_including(active_admin_namespace: an_instance_of(ActiveAdmin::Namespace)) + ) + ) + expect(ActiveSupport::Notifications).to( + receive(:instrument) + .with( + ActiveAdmin::Resource::RegisterEvent, + hash_including(active_admin_resource: an_instance_of(ActiveAdmin::Resource)) + ) + ) application.register Category, namespace: :not_yet_created end end @@ -52,5 +76,4 @@ expect(config_1.filters.size).to eq 2 end end - end diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index 4b62bbe2d49..85bdad99978 100644 --- a/spec/unit/resource_spec.rb +++ b/spec/unit/resource_spec.rb @@ -1,14 +1,17 @@ -require 'rails_helper' -require File.expand_path('config_shared_examples', File.dirname(__FILE__)) +# frozen_string_literal: true +require "rails_helper" +require File.expand_path("config_shared_examples", __dir__) module ActiveAdmin - describe Resource do - + RSpec.describe Resource do it_should_behave_like "ActiveAdmin::Resource" - before { load_defaults! } - let(:application){ ActiveAdmin::Application.new } - let(:namespace){ Namespace.new(application, :admin) } + around do |example| + with_resources_during(example) { namespace.register Category } + end + + let(:application) { ActiveAdmin::Application.new } + let(:namespace) { Namespace.new(application, :admin) } def config(options = {}) @config ||= Resource.new(namespace, Category, options) @@ -20,6 +23,7 @@ def config(options = {}) it "should return the resource's table name" do expect(config.resource_table_name).to eq '"categories"' end + context "when the :as option is given" do it "should return the resource's table name" do expect(config(as: "My Category").resource_table_name).to eq '"categories"' @@ -33,29 +37,34 @@ def config(options = {}) end end - describe '#decorator_class' do - it 'returns nil by default' do - expect(config.decorator_class).to be_nil + describe "#decorator_class" do + it "returns nil by default" do + expect(config.decorator_class).to eq nil end - context 'when a decorator is defined' do + + context "when a decorator is defined" do + around do |example| + with_resources_during(example) { resource } + end + let(:resource) { namespace.register(Post) { decorate_with PostDecorator } } - specify '#decorator_class_name should return PostDecorator' do - expect(resource.decorator_class_name).to eq '::PostDecorator' + specify "#decorator_class_name should return PostDecorator" do + expect(resource.decorator_class_name).to eq "::PostDecorator" end - it 'returns the decorator class' do + it "returns the decorator class" do expect(resource.decorator_class).to eq PostDecorator end end end - describe "controller name" do it "should return a namespaced controller name" do expect(config.controller_name).to eq "Admin::CategoriesController" end + context "when non namespaced controller" do - let(:namespace){ ActiveAdmin::Namespace.new(application, :root) } + let(:namespace) { ActiveAdmin::Namespace.new(application, :root) } it "should return a non namespaced controller name" do expect(config.controller_name).to eq "CategoriesController" end @@ -63,34 +72,35 @@ def config(options = {}) end describe "#include_in_menu?" do - let(:namespace){ ActiveAdmin::Namespace.new(application, :admin) } - subject{ resource } + subject { resource } + + around do |example| + with_resources_during(example) { resource } + end context "when regular resource" do - let(:resource){ namespace.register(Post) } + let(:resource) { namespace.register(Post) } it { is_expected.to be_include_in_menu } end context "when menu set to false" do - let(:resource){ namespace.register(Post){ menu false } } + let(:resource) { namespace.register(Post) { menu false } } it { is_expected.not_to be_include_in_menu } end end describe "#belongs_to" do - it "should build a belongs to configuration" do - expect(config.belongs_to_config).to be_nil + expect(config.belongs_to_config).to eq nil config.belongs_to :posts - expect(config.belongs_to_config).to_not be_nil + expect(config.belongs_to_config).to_not eq nil end - it "should set the target menu to the belongs to target" do + it "should not set the target menu to the belongs to target" do expect(config.navigation_menu_name).to eq ActiveAdmin::DEFAULT_MENU config.belongs_to :posts - expect(config.navigation_menu_name).to eq :posts + expect(config.navigation_menu_name).to eq ActiveAdmin::DEFAULT_MENU end - end describe "scoping" do @@ -133,6 +143,7 @@ def config(options = {}) expect(@resource.controller.new.send(:method_for_association_chain)).to eq :categories end end + context "when passing in the method as an option" do before do @resource = application.register Category do @@ -146,8 +157,9 @@ def config(options = {}) end end - describe "sort order" do + class MockResource + end context "when resource class responds to primary_key" do it "should sort by primary key desc by default" do @@ -168,39 +180,45 @@ def config(options = {}) config.sort_order = "task_id_desc" expect(config.sort_order).to eq "task_id_desc" end - end describe "adding a scope" do - it "should add a scope" do config.scope :published expect(config.scopes.first).to be_a(ActiveAdmin::Scope) expect(config.scopes.first.name).to eq "Published" + expect(config.scopes.first.show_count).to eq true + end + + context "when show_count disabled" do + it "should add a scope show_count = false" do + namespace.scopes_show_count = false + config.scope :published + expect(config.scopes.first.show_count).to eq false + end end - it "should retrive a scope by its id" do + it "should retrieve a scope by its id" do config.scope :published expect(config.get_scope_by_id(:published).name).to eq "Published" end it "should retrieve the default scope by proc" do - config.scope :published, default: proc{ true } + config.scope :published, default: proc { true } config.scope :all expect(config.default_scope.name).to eq "Published" end - end describe "#csv_builder" do context "when no csv builder set" do it "should return a default column builder with id and content columns" do - expect(config.csv_builder.exec_columns.size).to eq Category.content_columns.size + 1 + expect(config.csv_builder.exec_columns.size).to eq @config.content_columns.size + 1 end end context "when csv builder set" do - it "shuld return the csv_builder we set" do + it "should return the csv_builder we set" do csv_builder = CSVBuilder.new config.csv_builder = csv_builder expect(config.csv_builder).to eq csv_builder @@ -218,57 +236,60 @@ def config(options = {}) context "when breadcrumb is set" do context "when set to true" do before { config.breadcrumb = true } - it { is_expected.to be_truthy } + it { is_expected.to eq true } end context "when set to false" do before { config.breadcrumb = false } - it { is_expected.to be_falsey } + it { is_expected.to eq false } end end end - describe '#find_resource' do - let(:resource) { namespace.register(Post) } + describe "#find_resource" do let(:post) { double } - before do - if Rails::VERSION::MAJOR >= 4 - allow(Post).to receive(:find_by).with("id" => "12345") { post } - else - allow(Post).to receive(:find_by_id).with("12345") { post } - end + + around do |example| + with_resources_during(example) { resource } end - it 'can find the resource' do - expect(resource.find_resource('12345')).to eq post + context "without a decorator" do + let(:resource) { namespace.register(Post) } + + it "can find the resource" do + allow(Post).to receive(:find_by).with({ "id" => "12345" }) { post } + expect(resource.find_resource("12345")).to eq post + end end - context 'with a decorator' do + context "with a decorator" do let(:resource) { namespace.register(Post) { decorate_with PostDecorator } } - it 'decorates the resource' do - expect(resource.find_resource('12345')).to eq PostDecorator.new(post) + + it "decorates the resource" do + allow(Post).to receive(:find_by).with({ "id" => "12345" }) { post } + expect(resource.find_resource("12345")).to eq PostDecorator.new(post) + end + + it "does not decorate a not found resource" do + allow(Post).to receive(:find_by).with({ "id" => "54321" }) { nil } + expect(resource.find_resource("54321")).to equal nil end end - context 'when using a nonstandard primary key' do - let(:different_post) { double } + context "when using a nonstandard primary key" do + let(:resource) { namespace.register(Post) } + before do - allow(Post).to receive(:primary_key).and_return 'something_else' - if Rails::VERSION::MAJOR >= 4 - allow(Post).to receive(:find_by). - with("something_else" => "55555") { different_post } - else - allow(Post).to receive(:find_by_something_else). - with("55555") { different_post } - end + allow(Post).to receive(:primary_key).and_return "something_else" + allow(Post).to receive(:find_by).with({ "something_else" => "55555" }) { post } end - it 'can find the post by the custom primary key' do - expect(resource.find_resource('55555')).to eq different_post + it "can find the post by the custom primary key" do + expect(resource.find_resource("55555")).to eq post end end - context 'when using controller finder' do + context "when using controller finder" do let(:resource) do namespace.register(Post) do controller do @@ -277,51 +298,46 @@ def config(options = {}) end end - it 'can find the post by controller finder' do - allow(Post).to receive(:find_by_title!).with('title-name').and_return(post) + after do + Admin.send(:remove_const, :"PostsController") + end + + it "can find the post by controller finder" do + allow(Post).to receive(:find_by_title!).with("title-name").and_return(post) - expect(resource.find_resource('title-name')).to eq post + expect(resource.find_resource("title-name")).to eq post end end end describe "delegation" do - let(:controller) { + let(:controller) do Class.new do def method_missing(name, *args, &block) "called #{name}" end end.new - } - let(:resource) { ActiveAdmin::ResourceDSL.new(double, double) } + end + let(:resource) { ActiveAdmin::ResourceDSL.new(double) } before do expect(resource).to receive(:controller).and_return(controller) end - context "filters" do - [ - :before_filter, :skip_before_filter, - :after_filter, :skip_after_filter, - :around_filter, :skip_filter - ].each do |filter| - it "delegates #{filter}" do - expect(resource.send(filter)).to eq "called #{filter}" - end - end - end - - if Rails::VERSION::MAJOR == 4 - context "actions" do - [ - :before_action, :skip_before_action, - :after_action, :skip_after_action, - :around_action, :skip_action - ].each do |action| - it "delegates #{action}" do - expect(resource.send(action)).to eq "called #{action}" - end - end + %w[ + before_build after_build + before_create after_create + before_update after_update + before_save after_save + before_destroy after_destroy + 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 + actions + ].each do |method| + it "delegates #{method}" do + expect(resource.send(method)).to eq "called #{method}" end end end diff --git a/spec/unit/routing_spec.rb b/spec/unit/routing_spec.rb index db8f32fae6d..9cf0409a4a5 100644 --- a/spec/unit/routing_spec.rb +++ b/spec/unit/routing_spec.rb @@ -1,23 +1,28 @@ -# encoding: utf-8 +# frozen_string_literal: true +require "rails_helper" -require 'rails_helper' - -describe ActiveAdmin, "Routing", type: :routing do - - before do - load_defaults! - reload_routes! - end +RSpec.describe "Routing", type: :routing do + let(:namespaces) { ActiveAdmin.application.namespaces } it "should only have the namespaces necessary for route testing" do - expect(ActiveAdmin.application.namespaces.names).to eq [:admin, :root] + expect(namespaces.names).to eq [:admin] end - it "should route to the admin dashboard" do - expect(get('/admin')).to route_to 'admin/dashboard#index' + describe "admin dashboard" do + around do |example| + with_resources_during(example) {} + end + + it "should route to the admin dashboard" do + expect(get("/admin")).to route_to "admin/dashboard#index" + end end describe "root path helper" do + around do |example| + with_resources_during(example) {} + end + context "when in admin namespace" do it "should be admin_root_path" do expect(admin_root_path).to eq "/admin" @@ -25,7 +30,33 @@ end end + describe "route_options" do + around do |example| + with_resources_during(example) { ActiveAdmin.register(Post) } + end + + context "with a custom path set in route_options" do + before do + namespaces[:admin].route_options = { path: "/custom-path" } + reload_routes! + end + + after do + namespaces[:admin].route_options = {} + reload_routes! + end + + it "should route using the custom path" do + expect(admin_posts_path).to eq "/custom-path/posts" + end + end + end + describe "standard resources" do + around do |example| + with_resources_during(example) { ActiveAdmin.register(Post) } + end + context "when in admin namespace" do it "should route the index path" do expect(admin_posts_path).to eq "/admin/posts" @@ -45,8 +76,12 @@ end context "when in root namespace" do - before(:each) do - load_resources { ActiveAdmin.register(Post, namespace: false) } + around do |example| + with_resources_during(example) { ActiveAdmin.register(Post, namespace: false) } + end + + after do + namespaces.instance_variable_get(:@namespaces).delete(:root) end it "should route the index path" do @@ -68,55 +103,62 @@ context "with member action" do context "without an http verb" do - before do - load_resources do - ActiveAdmin.register(Post){ member_action "do_something" } + around do |example| + with_resources_during(example) do + ActiveAdmin.register(Post) { member_action "do_something" } end end it "should default to GET" do - expect({get: "/admin/posts/1/do_something"}).to be_routable - expect({post: "/admin/posts/1/do_something"}).to_not be_routable + expect({ get: "/admin/posts/1/do_something" }).to be_routable + expect({ post: "/admin/posts/1/do_something" }).to_not be_routable end end context "with one http verb" do - before do - load_resources do - ActiveAdmin.register(Post){ member_action "do_something", method: :post } + around do |example| + with_resources_during(example) do + ActiveAdmin.register(Post) { member_action "do_something", method: :post } end end it "should properly route" do - expect({post: "/admin/posts/1/do_something"}).to be_routable + expect({ post: "/admin/posts/1/do_something" }).to be_routable end end context "with two http verbs" do - before do - load_resources do - ActiveAdmin.register(Post){ member_action "do_something", method: [:put, :delete] } + around do |example| + with_resources_during(example) do + ActiveAdmin.register(Post) { member_action "do_something", method: [:put, :delete] } end end it "should properly route the first verb" do - expect({put: "/admin/posts/1/do_something"}).to be_routable + expect({ put: "/admin/posts/1/do_something" }).to be_routable end it "should properly route the second verb" do - expect({delete: "/admin/posts/1/do_something"}).to be_routable + expect({ delete: "/admin/posts/1/do_something" }).to be_routable end end end end describe "belongs to resource" do + around do |example| + with_resources_during(example) do + ActiveAdmin.register(User) + ActiveAdmin.register(Post) { belongs_to :user, optional: true } + end + end + it "should route the nested index path" do expect(admin_user_posts_path(1)).to eq "/admin/users/1/posts" end it "should route the nested show path" do - expect(admin_user_post_path(1,2)).to eq "/admin/users/1/posts/2" + expect(admin_user_post_path(1, 2)).to eq "/admin/users/1/posts/2" end it "should route the nested new path" do @@ -124,12 +166,12 @@ end it "should route the nested edit path" do - expect(edit_admin_user_post_path(1,2)).to eq "/admin/users/1/posts/2/edit" + expect(edit_admin_user_post_path(1, 2)).to eq "/admin/users/1/posts/2/edit" end context "with collection action" do - before do - load_resources do + around do |example| + with_resources_during(example) do ActiveAdmin.register(Post) do belongs_to :user, optional: true end @@ -141,39 +183,43 @@ it "should properly route the collection action" do expect({ get: "/admin/users/do_something" }).to \ - route_to({ controller: 'admin/users', action: 'do_something'}) + route_to({ controller: "admin/users", action: "do_something" }) end end end describe "page" do context "when default namespace" do - before(:each) do - load_resources { ActiveAdmin.register_page("Chocolate I lØve You!") } + around do |example| + with_resources_during(example) { ActiveAdmin.register_page("Chocolate I lØve You!") } end it "should route to the page under /admin" do expect(admin_chocolate_i_love_you_path).to eq "/admin/chocolate_i_love_you" end + end + + context "when in the root namespace" do + around do |example| + with_resources_during(example) { ActiveAdmin.register_page("Chocolate I lØve You!", namespace: false) } + end - context "when in the root namespace" do - before(:each) do - load_resources { ActiveAdmin.register_page("Chocolate I lØve You!", namespace: false) } - end + after do + namespaces.instance_variable_get(:@namespaces).delete(:root) + end - it "should route to page under /" do - expect(chocolate_i_love_you_path).to eq "/chocolate_i_love_you" - end + it "should route to page under /" do + expect(chocolate_i_love_you_path).to eq "/chocolate_i_love_you" end + end - context "when singular page name" do - before(:each) do - load_resources { ActiveAdmin.register_page("Log") } - end + context "when singular page name" do + around do |example| + with_resources_during(example) { ActiveAdmin.register_page("Log") } + end - it "should not inject _index_ into the route name" do - expect(admin_log_path).to eq "/admin/log" - end + it "should not inject _index_ into the route name" do + expect(admin_log_path).to eq "/admin/log" end end end diff --git a/spec/unit/scope_spec.rb b/spec/unit/scope_spec.rb index d944e06f0f4..0bf733c4044 100644 --- a/spec/unit/scope_spec.rb +++ b/spec/unit/scope_spec.rb @@ -1,161 +1,182 @@ -require 'rails_helper' - -describe ActiveAdmin::Scope do +# frozen_string_literal: true +require "rails_helper" +RSpec.describe ActiveAdmin::Scope do describe "creating a scope" do - subject{ scope } + subject { scope } context "when just a scope method" do - let(:scope) { ActiveAdmin::Scope.new :published } + let(:scope) { ActiveAdmin::Scope.new :published } - describe '#name' do + describe "#name" do subject { super().name } - it { is_expected.to eq("Published")} + it { is_expected.to eq("Published") } end - describe '#id' do + describe "#id" do subject { super().id } - it { is_expected.to eq("published")} + it { is_expected.to eq("published") } end - describe '#scope_method' do + describe "#scope_method" do subject { super().scope_method } it { is_expected.to eq(:published) } end end context "when scope method is :all" do - let(:scope) { ActiveAdmin::Scope.new :all } + let(:scope) { ActiveAdmin::Scope.new :all } - describe '#name' do + describe "#name" do subject { super().name } - it { is_expected.to eq("All")} + it { is_expected.to eq("All") } end - describe '#id' do + describe "#id" do subject { super().id } - it { is_expected.to eq("all")} + it { is_expected.to eq("all") } end # :all does not return a chain but an array of active record # instances. We set the scope_method to nil then. - describe '#scope_method' do + describe "#scope_method" do subject { super().scope_method } it { is_expected.to eq(nil) } end - describe '#scope_block' do + describe "#scope_block" do subject { super().scope_block } - it { is_expected.to eq(nil) } + it { is_expected.to eq(nil) } end end - context 'when a name and scope method is :all' do - let(:scope) { ActiveAdmin::Scope.new 'Tous', :all } + context "when a name and scope method is :all" do + let(:scope) { ActiveAdmin::Scope.new "Tous", :all } - describe '#name' do + describe "#name" do subject { super().name } - it { is_expected.to eq 'Tous' } + it { is_expected.to eq "Tous" } end - describe '#scope_method' do + describe "#scope_method" do subject { super().scope_method } - it { is_expected.to be_nil } + it { is_expected.to eq nil } end - describe '#scope_block' do + describe "#scope_block" do subject { super().scope_block } - it { is_expected.to be_nil } + it { is_expected.to eq nil } end end context "when a name and scope method" do - let(:scope) { ActiveAdmin::Scope.new "With API Access", :with_api_access } + let(:scope) { ActiveAdmin::Scope.new "With API Access", :with_api_access } - describe '#name' do + describe "#name" do subject { super().name } - it { is_expected.to eq("With API Access")} + it { is_expected.to eq("With API Access") } end - describe '#id' do + describe "#id" do subject { super().id } - it { is_expected.to eq("with_api_access")} + it { is_expected.to eq("with_api_access") } end - describe '#scope_method' do + describe "#scope_method" do subject { super().scope_method } it { is_expected.to eq(:with_api_access) } end end context "when a name and scope block" do - let(:scope) { ActiveAdmin::Scope.new("My Scope"){|s| s } } + let(:scope) { ActiveAdmin::Scope.new("My Scope") { |s| s } } - describe '#name' do + describe "#name" do subject { super().name } - it { is_expected.to eq("My Scope")} + it { is_expected.to eq("My Scope") } end - describe '#id' do + describe "#id" do subject { super().id } - it { is_expected.to eq("my_scope")} + it { is_expected.to eq("my_scope") } end - describe '#scope_method' do + describe "#scope_method" do subject { super().scope_method } it { is_expected.to eq(nil) } end - describe '#scope_block' do + describe "#scope_block" do subject { super().scope_block } - it { is_expected.to be_a(Proc)} + it { is_expected.to be_a(Proc) } end end context "when a name has a space and lowercase" do - let(:scope) { ActiveAdmin::Scope.new("my scope") } + let(:scope) { ActiveAdmin::Scope.new("my scope") } - describe '#name' do + describe "#name" do subject { super().name } - it { is_expected.to eq("my scope")} + it { is_expected.to eq("my scope") } end - describe '#id' do + describe "#id" do subject { super().id } - it { is_expected.to eq("my_scope")} + it { is_expected.to eq("my_scope") } end end context "with a proc as the label" do it "should raise an exception if a second argument isn't provided" do - expect{ ActiveAdmin::Scope.new proc{ Date.today.strftime '%A' } - }.to raise_error + expect do + ActiveAdmin::Scope.new proc { Date.today.strftime "%A" } + end.to raise_error "A string/symbol is required as the second argument if your label is a proc." end it "should properly render the proc" do - scope = ActiveAdmin::Scope.new proc{ Date.today.strftime '%A' }, :foobar - expect(scope.name).to eq Date.today.strftime '%A' + scope = ActiveAdmin::Scope.new proc { Date.today.strftime "%A" }, :foobar + expect(scope.name.call).to eq Date.today.strftime "%A" end end + context "with scope method and localizer" do + let(:localizer) do + loc = double(:localizer) + allow(loc).to receive(:t).with(:published, scope: "scopes").and_return("All published") + loc + end + let(:scope) { ActiveAdmin::Scope.new :published, :published, localizer: localizer } + + describe "#name" do + subject { super().name } + it { is_expected.to eq("All published") } + end + + describe "#id" do + subject { super().id } + it { is_expected.to eq("published") } + end + + describe "#scope_method" do + subject { super().scope_method } + it { is_expected.to eq(:published) } + end + end end # describe "creating a scope" describe "#display_if_block" do - it "should return true by default" do scope = ActiveAdmin::Scope.new(:default) expect(scope.display_if_block.call).to eq true end it "should return the :if block if set" do - scope = ActiveAdmin::Scope.new(:with_block, nil, if: proc{ false }) + scope = ActiveAdmin::Scope.new(:with_block, nil, if: proc { false }) expect(scope.display_if_block.call).to eq false end - end describe "#default" do - it "should accept a boolean" do scope = ActiveAdmin::Scope.new(:method, nil, default: true) expect(scope.default_block).to eq true @@ -167,24 +188,59 @@ end it "should store the :default proc" do - scope = ActiveAdmin::Scope.new(:with_block, nil, default: proc{ true }) + scope = ActiveAdmin::Scope.new(:with_block, nil, default: proc { true }) expect(scope.default_block.call).to eq true end - end describe "show_count" do - it "should allow setting of show_count to prevent showing counts" do scope = ActiveAdmin::Scope.new(:default, nil, show_count: false) expect(scope.show_count).to eq false end + it "should allow setting of show_count to query counts asynchronously" do + scope = ActiveAdmin::Scope.new(:default, nil, show_count: :async) + expect(scope.show_count).to eq :async + end + it "should set show_count to true if not passed in" do scope = ActiveAdmin::Scope.new(:default) expect(scope.show_count).to eq true end + end + + describe "#async_count?" do + it "should return true when show_count is :async" do + scope = ActiveAdmin::Scope.new(:default, nil, show_count: :async) + expect(scope.async_count?).to eq true + end + + it "should return false show_count is not passed in" do + scope = ActiveAdmin::Scope.new(:default) + expect(scope.async_count?).to eq false + end + it "should return false when show_count is false" do + scope = ActiveAdmin::Scope.new(:default, nil, show_count: false) + expect(scope.async_count?).to eq false + end end + describe "group" do + it "should default to nil" do + scope = ActiveAdmin::Scope.new(:default) + expect(scope.group).to eq nil + end + + it "should accept a symbol to assign a group to the scope" do + scope = ActiveAdmin::Scope.new(:default, nil, group: :test) + expect(scope.group).to eq :test + end + + it "should accept a string to assign a group to the scope" do + scope = ActiveAdmin::Scope.new(:default, nil, group: "test") + expect(scope.group).to eq :test + end + end end diff --git a/spec/unit/settings_node_spec.rb b/spec/unit/settings_node_spec.rb new file mode 100644 index 00000000000..c5fbe60d5e0 --- /dev/null +++ b/spec/unit/settings_node_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::SettingsNode do + subject { ActiveAdmin::SettingsNode.build } + let!(:child) { ActiveAdmin::SettingsNode.build(subject) } + + context "parent setting includes foo" do + before { subject.register :foo, true } + + it "returns parent settings" do + expect(child.foo).to eq true + end + + it "fails if setting undefined" do + expect do + child.bar + end.to raise_error(NoMethodError) + end + + context "child overrides foo" do + before { child.foo = false } + + it { expect(child.foo).to eq false } + end + end +end diff --git a/spec/unit/settings_spec.rb b/spec/unit/settings_spec.rb deleted file mode 100644 index 709843f3a64..00000000000 --- a/spec/unit/settings_spec.rb +++ /dev/null @@ -1,118 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::Settings do - - subject{ Class.new{ include ActiveAdmin::Settings } } - - it{ is_expected.to respond_to :setting } - it{ is_expected.to respond_to :deprecated_setting } - it{ is_expected.to respond_to :default_settings } - - describe "class API" do - it "should create settings" do - subject.setting :foo, 'bar' - expect(subject.default_settings[:foo]).to eq 'bar' - end - - it "should create deprecated settings" do - expect(ActiveAdmin::Deprecation).to receive(:deprecate).twice - subject.deprecated_setting :baz, 32 - expect(subject.default_settings[:baz]).to eq 32 - end - end - - describe "instance API" do - - before do - subject.setting :foo, 'bar' - subject.deprecated_setting :baz, 32 - end - let(:instance) { subject.new } - - it "should have access to a default value" do - expect(instance.foo).to eq 'bar' - instance.foo = 'qqq' - expect(instance.foo).to eq 'qqq' - end - - it "should have access to a deprecated value" do - expect(ActiveAdmin::Deprecation).to receive(:warn).exactly(3).times - expect(instance.baz).to eq 32 - instance.baz = [45] - expect(instance.baz).to eq [45] - end - end - -end - - -describe ActiveAdmin::Settings::Inheritance do - - subject do - Class.new do - include ActiveAdmin::Settings - include ActiveAdmin::Settings::Inheritance - end - end - - it{ is_expected.to respond_to :settings_inherited_by } - it{ is_expected.to respond_to :inheritable_setting } - it{ is_expected.to respond_to :deprecated_inheritable_setting } - - let(:heir) { Class.new } - - before do - subject.settings_inherited_by heir - end - - describe "class API" do - it "should add setting to an heir" do - subject.inheritable_setting :one, 2 - expect(heir.default_settings[:one]).to eq 2 - end - - it "should add deprecated setting to an heir" do - expect(ActiveAdmin::Deprecation).to receive(:deprecate).exactly(4).times - subject.deprecated_inheritable_setting :three, 4 - expect(heir.default_settings[:three]).to eq 4 - end - end - - describe "instance API" do - describe "the setter `config.left =`" do - before{ subject.inheritable_setting :left, :right } - it "should work" do - config = heir.new - config.left = :none - expect(config.left).to eq :none - end - end - - describe "the getter `config.left`" do - before{ subject.inheritable_setting :left, :right } - it "should work" do - expect(heir.new.left).to eq :right - end - end - - describe "the getter with question-mark `config.left?`" do - { - "nil" => [nil, false], - "false" => [false, false], - "true" => [true, true], - "string" => ["string", true], - "empty string" => ["", false], - "array" => [[1, 2], true], - "empty array" => [[], false] - }.each do |context, (value, result)| - context "with a #{context} value" do - before{ subject.inheritable_setting :left, value } - it "should be #{result}" do - expect(heir.new.left?).to eq result - end - end - end - end - end - -end diff --git a/spec/unit/view_factory_spec.rb b/spec/unit/view_factory_spec.rb deleted file mode 100644 index 06f605f3236..00000000000 --- a/spec/unit/view_factory_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'rails_helper' - -def it_should_have_view(key, value) - it "should have #{value} for view key '#{key}'" do - expect(subject.send(key)).to eq value - end -end - -describe ActiveAdmin::ViewFactory do - - it_should_have_view :global_navigation, ActiveAdmin::Views::TabbedNavigation - it_should_have_view :utility_navigation, ActiveAdmin::Views::TabbedNavigation - it_should_have_view :site_title, ActiveAdmin::Views::SiteTitle - it_should_have_view :action_items, ActiveAdmin::Views::ActionItems - it_should_have_view :header, ActiveAdmin::Views::Header - it_should_have_view :blank_slate, ActiveAdmin::Views::BlankSlate - it_should_have_view :layout, ActiveAdmin::Views::Pages::Layout - -end diff --git a/spec/unit/view_helpers/display_name_spec.rb b/spec/unit/view_helpers/display_name_spec.rb deleted file mode 100644 index 21223c2d2a3..00000000000 --- a/spec/unit/view_helpers/display_name_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -require 'rails_helper' - -describe "display_name" do - - include ActiveAdmin::ViewHelpers - - ActiveAdmin::Application.new.display_name_methods.map(&:to_s).each do |m| - it "should return #{m} when defined" do - klass = Class.new do - define_method(m) { m } - end - expect(display_name klass.new).to eq m - end - end - - it "should memoize the result for the class" do - subject = Class.new.new - expect(subject).to receive(:name).twice.and_return "My Name" - expect(display_name subject).to eq "My Name" - expect(ActiveAdmin.application).to_not receive(:display_name_methods) - expect(display_name subject).to eq "My Name" - end - - it "should not call a method if it's an association" do - klass = Class.new - subject = klass.new - allow(klass).to receive(:reflect_on_all_associations).and_return [ double(name: :login) ] - allow(subject).to receive :login - expect(subject).to_not receive :login - allow(subject).to receive(:email).and_return 'foo@bar.baz' - expect(display_name subject).to eq 'foo@bar.baz' - end - - it "should return `nil` when the passed object is `nil`" do - expect(display_name nil).to eq nil - end - - it "should return 'false' when the passed objct is `false`" do - expect(display_name false).to eq "false" - end - - it "should default to `to_s`" do - subject = Class.new.new - expect(display_name subject).to eq subject.to_s - end - - context "when no display name method is defined" do - context "on a Rails model" do - it "should show the model name" do - class ThisModel - extend ActiveModel::Naming - end - subject = ThisModel.new - expect(display_name subject).to eq "This model" - end - - it "should show the model name, plus the ID if in use" do - subject = Tagging.create! - expect(display_name subject).to eq "Tagging #1" - end - - it "should translate the model name" do - with_translation activerecord: {models: {tagging: {one: "Different"}}} do - subject = Tagging.create! - expect(display_name subject).to eq "Different #1" - end - end - end - end - -end diff --git a/spec/unit/view_helpers/download_format_links_helper_spec.rb b/spec/unit/view_helpers/download_format_links_helper_spec.rb deleted file mode 100644 index 9181d0d4ccc..00000000000 --- a/spec/unit/view_helpers/download_format_links_helper_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::ViewHelpers::DownloadFormatLinksHelper do - - describe "class methods" do - before :all do - - begin - # The mime type to be used in respond_to |format| style web-services in rails - Mime::Type.register "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", :xlsx - rescue NameError - puts "Mime module not defined. Skipping registration of xlsx" - end - - class Foo - include ActiveAdmin::ViewHelpers::DownloadFormatLinksHelper - end - end - - it "extends the class to add a formats class method that returns the default formats." do - expect(Foo.formats).to eq [:csv, :xml, :json] - end - - it "does not let you alter the formats array directly" do - Foo.formats << :xlsx - expect(Foo.formats).to eq [:csv, :xml, :json] - end - - it "allows us to add new formats" do - Foo.add_format :xlsx - expect(Foo.formats).to eq [:csv, :xml, :json, :xlsx] - end - - it "raises an exception if you provide an unregisterd mime type extension" do - expect{ Foo.add_format :hoge }.to raise_error - end - - end -end diff --git a/spec/unit/view_helpers/fields_for_spec.rb b/spec/unit/view_helpers/fields_for_spec.rb deleted file mode 100644 index 3f1acd6310a..00000000000 --- a/spec/unit/view_helpers/fields_for_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'active_admin/view_helpers/fields_for' - -describe ActiveAdmin::ViewHelpers::FormHelper, ".fields_for" do - include ActiveAdmin::ViewHelpers::FormHelper - - it "should skip :action, :controller and :commit" do - expect(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(fields_for_params({scope: "All", name: "Greg"}, except: :name)). - to eq [ { scope: "All" } ] - end - - it "should allow an array for the except" do - expect(fields_for_params({scope: "All", name: "Greg", age: "12"}, except: [:name, :age])). - to eq [ { scope: "All" } ] - end - - it "should work with hashes" do - params = 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(fields_for_params(filters: { user: { name: "John" }})). - to eq [ { "filters[user][name]" => "John" } ] - end - - it "should work with arrays" do - expect(fields_for_params(people: ["greg", "emily", "philippe"])). - to eq [ { "people[]" => "greg" }, - { "people[]" => "emily" }, - { "people[]" => "philippe" } ] - end - - it "should work with symbols" do - expect(fields_for_params(filter: :id)). - to eq [ { filter: "id" } ] - end - - it "should work with booleans" do - expect(fields_for_params(booleantest: false)).to eq [ { booleantest: false } ] - end -end diff --git a/spec/unit/view_helpers/flash_helper_spec.rb b/spec/unit/view_helpers/flash_helper_spec.rb deleted file mode 100644 index 7d72802396e..00000000000 --- a/spec/unit/view_helpers/flash_helper_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::ViewHelpers::FlashHelper do - - describe '.flash_messages' do - let(:view) { action_view } - - it "should not include 'timedout' flash messages by default" do - view.request.flash[:alert] = "Alert" - view.request.flash[:timedout] = true - expect(view.flash_messages).to include 'alert' - expect(view.flash_messages).to_not include 'timedout' - end - - it "should not return flash messages included in flash_keys_to_except" do - expect(view.active_admin_application).to receive(:flash_keys_to_except).and_return ["hideme"] - view.request.flash[:alert] = "Alert" - view.request.flash[:hideme] = "Do not show" - expect(view.flash_messages).to include 'alert' - expect(view.flash_messages).to_not include 'hideme' - end - - end -end - diff --git a/spec/unit/view_helpers/form_helper_spec.rb b/spec/unit/view_helpers/form_helper_spec.rb deleted file mode 100644 index e97964ee9a4..00000000000 --- a/spec/unit/view_helpers/form_helper_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::ViewHelpers::FormHelper do - - describe '.active_admin_form_for' do - let(:view) { action_view } - let(:resource) { double 'resource' } - let(:default_options) { { builder: ActiveAdmin::FormBuilder } } - - it 'calls semantic_form_for with the ActiveAdmin form builder' do - expect(view).to receive(:semantic_form_for).with(resource, builder: ActiveAdmin::FormBuilder) - view.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(view).to receive(:semantic_form_for).with(resource, builder: custom_builder) - view.active_admin_form_for(resource, builder: custom_builder) - end - end - - describe ".hidden_field_tags_for" do - let(:view) { action_view } - - it "should render hidden field tags for params" do - html = Capybara.string view.hidden_field_tags_for(scope: "All", filter: "None") - expect(html).to have_selector("input#hidden_active_admin_scope[name=scope][type=hidden][value=All]") - expect(html).to have_selector("input#hidden_active_admin_filter[name=filter][type=hidden][value=None]") - end - - it "should generate not default id for hidden input" do - expect(view.hidden_field_tags_for(scope: "All")[/id="([^"]+)"/, 1]).to_not eq "scope" - end - - it "should filter out the field passed via the option :except" do - html = Capybara.string view.hidden_field_tags_for({scope: "All", filter: "None"}, except: :filter) - expect(html).to have_selector("input#hidden_active_admin_scope[name=scope][type=hidden][value=All]") - end - end -end - diff --git a/spec/unit/view_helpers/method_or_proc_helper_spec.rb b/spec/unit/view_helpers/method_or_proc_helper_spec.rb index 41a1e7de2a6..040c28b7a99 100644 --- a/spec/unit/view_helpers/method_or_proc_helper_spec.rb +++ b/spec/unit/view_helpers/method_or_proc_helper_spec.rb @@ -1,16 +1,16 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe MethodOrProcHelper do +RSpec.describe MethodOrProcHelper do let(:receiver) { double } let(:context) do obj = double receiver_in_context: receiver - obj.extend MethodOrProcHelper + obj.extend described_class obj end describe "#call_method_or_exec_proc" do - it "should call the method in the context when a symbol" do expect(context.call_method_or_exec_proc(:receiver_in_context)).to eq receiver end @@ -20,43 +20,40 @@ end it "should exec a proc in the context" do - test_proc = Proc.new{ raise "Success" if receiver_in_context } + test_proc = Proc.new { raise "Success" if receiver_in_context } - expect { + expect do context.call_method_or_exec_proc(test_proc) - }.to raise_error("Success") + end.to raise_error("Success") end - end describe "#call_method_or_proc_on" do - - [:hello, 'hello'].each do |key| + [:hello, "hello"].each do |key| context "when a #{key.class}" do it "should call the method on the receiver" do - expect(receiver).to receive(key).and_return 'hello' + expect(receiver).to receive(key).and_return "hello" - expect(context.call_method_or_proc_on(receiver, key)).to eq 'hello' + expect(context.call_method_or_proc_on(receiver, key)).to eq "hello" end it "should receive additional arguments" do - expect(receiver).to receive(key).with(:world).and_return 'hello world' + expect(receiver).to receive(key).with(:world).and_return "hello world" - expect(context.call_method_or_proc_on(receiver, key, :world)).to eq 'hello world' + expect(context.call_method_or_proc_on(receiver, key, :world)).to eq "hello world" end end end context "when a proc" do - it "should exec the block in the context and pass in the receiver" do test_proc = Proc.new do |arg| raise "Success!" if arg == receiver_in_context end - expect { - context.call_method_or_proc_on(receiver,test_proc) - }.to raise_error("Success!") + expect do + context.call_method_or_proc_on(receiver, test_proc) + end.to raise_error("Success!") end it "should receive additional arguments" do @@ -64,15 +61,13 @@ raise "Success!" if arg1 == receiver_in_context && arg2 == "Hello" end - expect { + expect do context.call_method_or_proc_on(receiver, test_proc, "Hello") - }.to raise_error("Success!") + end.to raise_error("Success!") end - end context "when a proc and exec: false" do - it "should call the proc and pass in the receiver" do obj_not_in_context = double @@ -80,18 +75,15 @@ raise "Success!" if arg == receiver && obj_not_in_context end - expect { - context.call_method_or_proc_on(receiver,test_proc, exec: false) - }.to raise_error("Success!") + expect do + context.call_method_or_proc_on(receiver, test_proc, exec: false) + end.to raise_error("Success!") end - end - - end describe "#render_or_call_method_or_proc_on" do - [ :symbol, Proc.new{} ].each do |key| + [ :symbol, Proc.new {} ].each do |key| context "when a #{key.class}" do it "should call #call_method_or_proc_on" do options = { foo: :bar } @@ -112,7 +104,7 @@ let(:args) { [1, 2, 3] } context "when a Proc" do - let(:object) { Proc.new { } } + let(:object) { Proc.new {} } it "should instance_exec the Proc" do expect(receiver).to receive(:instance_exec).with(args, &object).and_return("data") @@ -135,5 +127,4 @@ end end end - end diff --git a/spec/unit/views/components/attributes_table_spec.rb b/spec/unit/views/components/attributes_table_spec.rb index b083a7f0f9c..2cea8344526 100644 --- a/spec/unit/views/components/attributes_table_spec.rb +++ b/spec/unit/views/components/attributes_table_spec.rb @@ -1,68 +1,77 @@ -require 'rails_helper' - -describe ActiveAdmin::Views::AttributesTable do +# frozen_string_literal: true +require "rails_helper" +RSpec.describe ActiveAdmin::Views::AttributesTable do describe "creating with the dsl" do - let(:helpers) { action_view } + let(:helpers) { mock_action_view } let(:post) do post = Post.new title: "Hello World", body: nil - allow(post).to receive(:id){ 1 } - allow(post).to receive(:new_record?){ false } + allow(post).to receive(:id) { 1 } + allow(post).to receive(:new_record?) { false } post end - let(:assigns){ { post: post } } + let(:assigns) { { post: post } } # Loop through a few different ways to make the same table # and ensure that they produce the same results { "when attributes are passed in to the builder methods" => proc { - render_arbre_component(assigns) { + render_arbre_component(assigns) do attributes_table_for post, :id, :title, :body - } + end }, "when attributes are built using the block" => proc { - render_arbre_component(assigns) { + render_arbre_component(assigns) do attributes_table_for post do rows :id, :title, :body end - } + end }, "when each attribute is passed in by itself" => proc { - render_arbre_component(assigns) { + render_arbre_component(assigns) do attributes_table_for post do row :id row :title row :body end - } + end }, "when you create each row with a custom block" => proc { - render_arbre_component(assigns) { + render_arbre_component(assigns) do attributes_table_for post do - row("Id") { post.id } - row("Title"){ post.title } + row("Id") { post.id } + row("Title") { post.title } row("Body") { post.body } end - } + end + }, + "when you create each row with a string and symbol" => proc { + render_arbre_component(assigns) do + attributes_table_for post do + row "Id", :id + row "Title", :title + row "Body", :body + end + end }, "when you create each row with a custom block that returns nil" => proc { - render_arbre_component(assigns) { + render_arbre_component(assigns) do attributes_table_for post do - row("Id") { text_node post.id; nil } - row("Title"){ text_node post.title; nil } + row("Id") { text_node post.id; nil } + row("Title") { text_node post.title; nil } row("Body") { text_node post.body; nil } end - } + end }, - }.each do |context_title, table_decleration| + }.each do |context_title, table_declaration| context context_title do - let(:table) { instance_eval &table_decleration } + let(:table) { instance_eval(&table_declaration) } - it "should render a div wrapper with the class '.attributes_table'" do - expect(table.tag_name).to eq 'div' - expect(table.attr(:class)).to include('attributes_table') + it "should render a div wrapper with the class '.attributes-table'" do + expect(table.tag_name).to eq "div" + expect(table.attr(:class)).to include("attributes-table") end it "should add id and type class" do @@ -76,12 +85,12 @@ describe "rendering the rows" do [ - ["Id" , "1"], - ["Title" , "Hello World"], - ["Body" , "Empty"] + ["Id", "1"], + ["Title", "Hello World"], + ["Body", "Empty"] ].each_with_index do |(title, content), i| describe "for #{title}" do - let(:current_row){ table.find_by_tag("tr")[i] } + let(:current_row) { table.find_by_tag("tr")[i] } it "should have the title '#{title}'" do expect(current_row.find_by_tag("th").first.content).to eq title @@ -93,63 +102,62 @@ end end end # describe rendering rows - end end # describe dsl styles - it "should add a class for each row based on the col name" do - table = render_arbre_component(assigns) { + it "should add a data attribute for each row based on the column name" do + table = render_arbre_component(assigns) do attributes_table_for(post) do row :title row :created_at end - } - expect(table.find_by_tag("tr").first.to_s. - split("\n").first.lstrip). - to eq '' - - expect(table.find_by_tag("tr").last.to_s. - split("\n").first.lstrip). - to eq '' + end + + expect(table.find_by_tag("tr").first.to_s.split("\n").first.lstrip). + to eq '' + + 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 - } + end expect(table.find_by_tag("tr").first.to_s.split("\n").first.lstrip). - to eq '' + 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 - } + end expect(table.find_by_tag("td").first.content.chomp.strip).to eq "1" end - context 'an attribute ending in _id' do + 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' + post.author = User.new username: "john_doe", first_name: "John", last_name: "Doe" end - it 'should call the association if one exists' do + it "should call the association if one exists" do table = render_arbre_component assigns do - attributes_table_for post, :author_id + 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' + 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 nonexistant association' do + + 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' + expect(table.find_by_tag("th").first.content).to eq "Foo" + expect(table.find_by_tag("td").first.content).to eq "23" end end @@ -170,14 +178,14 @@ end it "does not set id on the table" do - expect(table.attr(:id)).to be_nil + 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) + expect(cols.size).to eq(2 + 1) end it "assigns an id to each col" do @@ -185,18 +193,6 @@ expect(col.id).to eq "attributes_table_post_#{index + 1}" end end - - it "assigns a class to each col" do - cols[1..-1].each_with_index do |col, index| - expect(col.class_names).to include("post") - end - end - - it "assigns alternation classes to each col" do - cols[1..-1].each_with_index do |col, index| - expect(col.class_names).to include(["even", "odd"][index % 2]) - end - end end context "when rendering the rows" do @@ -205,13 +201,12 @@ end [ - ["Id" , "1", "2"], + ["Id", "1", "2"], ["Title", "Hello World", "Multi Column"], ].each_with_index do |set, i| describe "for #{set[0]}" do - let(:title){ set[0] } - let(:content){ set[1] } - let(:current_row){ table.find_by_tag("tr")[i] } + 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 @@ -219,8 +214,10 @@ context "with defined attribute name translation" do it "should have the translated attribute name in the title" do - with_translation activerecord: {attributes: {post: {title: 'Translated Title', id: 'Translated Id'}}} do - expect(current_row.find_by_tag("th").first.content).to eq "Translated #{title}" + 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 @@ -235,7 +232,6 @@ end # describe rendering rows end # with a collection - context "when using a single Hash" do let(:table) do render_arbre_component nil, helpers do @@ -256,7 +252,7 @@ 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 + attributes_table_for [{ foo: 1 }, { foo: 2 }] do row :foo end end @@ -267,6 +263,26 @@ expect(table.find_by_tag("td")[1].content).to eq "2" end 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_selector_spec.rb b/spec/unit/views/components/batch_action_selector_spec.rb deleted file mode 100644 index 9694ff72ee3..00000000000 --- a/spec/unit/views/components/batch_action_selector_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'rails_helper' -require 'active_admin/batch_actions/views/batch_action_selector' - -describe ActiveAdmin::BatchActions::BatchActionSelector do - - let(:dropdown) do - render_arbre_component do - batch_action_selector [ - ActiveAdmin::BatchAction.new(:action_1, "Action 1"), - ActiveAdmin::BatchAction.new(:action_2, "Action 2"), - ActiveAdmin::BatchAction.new(:action_3, "Action 3") - ] - end - end - - describe "the action list" do - subject do - dropdown.find_by_class("dropdown_menu_list").first - end - - describe '#tag_name' do - subject { super().tag_name } - it { is_expected.to eql("ul") } - end - - describe '#content' do - subject { super().content } - it{ is_expected.to include("class=\"batch_action\" data-action=\"action_1\"") } - end - - describe '#content' do - subject { super().content } - it{ is_expected.to include("class=\"batch_action\" data-action=\"action_2\"") } - end - - describe '#content' do - subject { super().content } - it{ is_expected.to include("class=\"batch_action\" data-action=\"action_3\"") } - end - - 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 c58bdaadd6b..00000000000 --- a/spec/unit/views/components/blank_slate_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'rails_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 - - describe '#tag_name' do - subject { super().tag_name } - it { is_expected.to eql 'div' } - end - - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('blank_slate_container') } - end - - describe '#content' do - subject { super().content } - it { is_expected.to include 'There are no Posts yet. Create one' } - end - 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 41e8f016622..00000000000 --- a/spec/unit/views/components/columns_spec.rb +++ /dev/null @@ -1,189 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::Views::Columns do - - describe "Rendering zero columns" do - let(:cols) do - render_arbre_component do - columns do - end - end - end - - it "should have the class .columns" do - expect(cols.class_list).to include("columns") - end - - it "should have one column" do - expect(cols.children.first.class_list).not_to include("column") - end - end - - 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 - expect(cols.class_list).to include("columns") - end - - it "should have one column" do - expect(cols.children.size).to eq 1 - expect(cols.children.first.class_list).to include("column") - end - - it "should have one column with the width 100.0%" do - expect(cols.children.first.attr(:style)).to 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 - expect(cols.children.size).to eq 2 - end - - it "should have a first column with width 49% and margin 2%" do - expect(cols.children.first.attr(:style)).to eq "width: 49.0%; margin-right: 2%;" - end - - it "should have a second column with width 49% and no right margin" do - expect(cols.children.last.attr(:style)).to eq "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 - expect(cols.children.size).to eq 4 - end - - - (0..2).to_a.each do |index| - it "should have column #{index + 1} with width 49% and margin 2%" do - expect(cols.children[index].attr(:style)).to eq "width: 23.5%; margin-right: 2%;" - end - end - - it "should have column 4 with width 49% and no margin" do - expect(cols.children[3].attr(:style)).to eq "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 - expect(cols.children.first.attr(:style)).to eq "width: 49.0%; margin-right: 2%;" - end - - it "should default to 1 if not passed in" do - expect(cols.children.last.attr(:style)).to eq "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 - expect(cols.children.first.attr(:style)).to eq "width: 49.0%; max-width: 100px; margin-right: 2%;" - end - - it "should omit the value if not present" do - expect(cols.children.last.attr(:style)).to eq "width: 49.0%;" - end - - context "when passed an integer value" do - let(:cols) do - render_arbre_component do - columns do - column(max_width: 100){ "Hello World" } - column(){ "Hello World" } - end - end - end - - it "should be treated as pixels" do - expect(cols.children.first.attr(:style)).to eq "width: 49.0%; max-width: 100px; margin-right: 2%;" - end - 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 - expect(cols.children.first.attr(:style)).to eq "width: 49.0%; min-width: 100px; margin-right: 2%;" - end - - it "should omit the value if not present" do - expect(cols.children.last.attr(:style)).to eq "width: 49.0%;" - end - - context "when passed an integer value" do - let(:cols) do - render_arbre_component do - columns do - column(min_width: 100){ "Hello World" } - column(){ "Hello World" } - end - end - end - - it "should be treated as pixels" do - expect(cols.children.first.attr(:style)).to eq "width: 49.0%; min-width: 100px; margin-right: 2%;" - end - end - - end - -end diff --git a/spec/unit/views/components/index_list_spec.rb b/spec/unit/views/components/index_list_spec.rb index ec72f760336..a6c0be2b316 100644 --- a/spec/unit/views/components/index_list_spec.rb +++ b/spec/unit/views/components/index_list_spec.rb @@ -1,35 +1,64 @@ -require 'rails_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 - allow(helpers).to receive(:url_for).and_return("/") - allow(helpers).to receive(: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 - describe '#tag_name' do + describe "#tag_name" do subject { super().tag_name } - it { is_expected.to eq 'ul'} + 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") expect(a_tags.size).to eq 2 expect(a_tags.first.to_s).to include("Table") - expect(a_tags.last.to_s).to include("List") + 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 index 990772791bd..9ab6ba25240 100644 --- a/spec/unit/views/components/index_table_for_spec.rb +++ b/spec/unit/views/components/index_table_for_spec.rb @@ -1,126 +1,96 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Views::IndexAsTable::IndexTableFor do - describe 'creating with the dsl' do +RSpec.describe ActiveAdmin::Views::IndexAsTable::IndexTableFor do + describe "creating with the dsl" do let(:collection) do - [Post.new(title: 'First Post', starred: true)] + [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.batch_actions = [ActiveAdmin::BatchAction.new(:flag, "Flag") {}] namespace end let(:assigns) do { collection: collection, - active_admin_config: active_admin_config + active_admin_config: active_admin_config, + resource_class: User, } end let(:helpers) { mock_action_view } - context 'when creating a selectable column' do + 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 + 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 + context "creates a table header based on the selectable column" do let(:header) do - table.find_by_tag('th').first + table.find_by_tag("th").first end - it 'with selectable column class name' do - expect(header.attributes[:class]).to include 'col-selectable' + it "with selectable column class name" do + expect(header.attributes[:class]).to include "selectable" end - it 'not sortable' do - expect(header.attributes[:class]).not_to include 'sortable' + it "not sortable" do + expect(header.attributes).not_to include("data-sortable": "") end end end - context 'when creating an index column' do - let(:base_collection) do - posts = [ - Post.new(title: 'First Post', starred: true), - Post.new(title: 'Second Post', starred: true), - Post.new(title: 'Third Post', starred: true), - Post.new(title: 'Fourth Post', starred: true) - ] - Kaminari.paginate_array(posts, limit: 2) - end - let(:collection) do - base_collection.page(1) - end + context "when creating an id column" do + before { allow(helpers).to receive(:url_target) { 'routing_stub' } } - let(:table) do + def build_index_table(&block) render_arbre_component assigns, helpers do - insert_tag(ActiveAdmin::Views::IndexAsTable::IndexTableFor, collection, {sortable: true}) do - index_column + insert_tag(ActiveAdmin::Views::IndexAsTable::IndexTableFor, collection, { sortable: true }) do + instance_exec(&block) end end end - context 'creates a table header based on the index column' do - let(:header) do - table.find_by_tag('th').first - end - - it 'with index column class name' do - expect(header.attributes[:class]).to include 'col-index' - end - - it 'not sortable' do - expect(header.attributes[:class]).not_to include 'sortable' - 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 - context 'verifying the indices for the rows' do - let(:index_values) do - table.find_by_tag('tr').map do |row| - next unless row.find_by_tag('td').first - row.find_by_tag('td').first.content - end.compact - 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 - context 'viewing the first page' do - it 'shows the correct indices' do - expect(index_values).to eq(['1', '2']) - end - end - context 'viewing the second page' do - let(:collection) do - base_collection.page(2) - end - it 'shows the correct indices' do - expect(index_values).to eq(['3', '4']) - end - 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 - context 'allows for zero-based indices' do - let(:table) do - render_arbre_component assigns, helpers do - insert_tag(ActiveAdmin::Views::IndexAsTable::IndexTableFor, collection, {sortable: true}) do - index_column(0) - end - end - 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 - let(:index_values) do - table.find_by_tag('tr').map do |row| - next unless row.find_by_tag('td').first - row.find_by_tag('td').first.content - end.compact - 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 'shows the correct indices' do - expect(index_values).to eq(['0', '1']) - 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 diff --git a/spec/unit/views/components/paginated_collection_spec.rb b/spec/unit/views/components/paginated_collection_spec.rb index f4f0804618f..b4e36669d85 100644 --- a/spec/unit/views/components/paginated_collection_spec.rb +++ b/spec/unit/views/components/paginated_collection_spec.rb @@ -1,23 +1,23 @@ -require 'rails_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 - 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.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 @@ -29,65 +29,65 @@ def paginated_collection(*args) before do allow(collection).to receive(:except) { collection } unless collection.respond_to? :except - allow(collection).to receive(:group_values) { [] } unless collection.respond_to? :group_values + allow(collection).to receive(:group_values) { [] } unless collection.respond_to? :group_values end - let(:pagination){ paginated_collection collection } + let(:pagination) { paginated_collection collection } it "should set :collection as the passed in collection" do - expect(pagination.find_by_class('pagination_information').first.content).to eq "Displaying all 3 posts" + 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 - expect { + expect do paginated_collection([Post.new, Post.new]) - }.to raise_error(StandardError, "Collection is not a paginated scope. Set collection.page(params[:page]).per(10) before calling :paginated_collection.") + 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' + 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' + 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.map{ 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) } it "should customize the page number parameter in pagination links" do - expect(pagination.children.last.content).to 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 = 10.times.map{ 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, params: { anchor: 'here' }) } + let(:pagination) { paginated_collection(collection, param_name: :post_page, params: { anchor: "here" }) } it "should pass it through to Kaminari" do - expect(pagination.children.last.content).to match(/\/admin\/posts\?post_page=2#here/) + expect(pagination.to_s).to match(/\/admin\/posts\?post_page=2#here/) end end context "when specifying download_links: false option" do let(:collection) do - posts = 10.times.map{ 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) } it "should not render download links" do - expect(pagination.find_by_tag('div').last.content).to_not match(/Download:/) + expect(pagination.find_by_tag("div").last.content).to_not match(/Download:/) end end @@ -100,7 +100,7 @@ def paginated_collection(*args) let(:pagination) { paginated_collection(collection, entry_name: "message") } it "should use :entry_name as the collection name" do - expect(pagination.find_by_class('pagination_information').first.content).to eq "Displaying 1 message" + expect(pagination.find_by_class("pagination-information").first.content).to eq "Showing 1 of 1" end end @@ -108,7 +108,7 @@ def paginated_collection(*args) let(:pagination) { paginated_collection(collection, entry_name: "message") } it "should use :entry_name as the collection name" do - expect(pagination.find_by_class('pagination_information').first.content).to eq "Displaying all 3 messages" + expect(pagination.find_by_class("pagination-information").first.content).to eq "Showing all 3" end end @@ -121,7 +121,7 @@ def paginated_collection(*args) let(:pagination) { paginated_collection(collection, entry_name: "singular", entries_name: "plural") } it "should use :entry_name as the collection name" do - expect(pagination.find_by_class('pagination_information').first.content).to eq "Displaying 1 singular" + expect(pagination.find_by_class("pagination-information").first.content).to eq "Showing 1 of 1" end end @@ -129,7 +129,7 @@ def paginated_collection(*args) let(:pagination) { paginated_collection(collection, entry_name: "singular", entries_name: "plural") } it "should use :entries_name as the collection name" do - expect(pagination.find_by_class('pagination_information').first.content).to eq "Displaying all 3 plural" + expect(pagination.find_by_class("pagination-information").first.content).to eq "Showing all 3" end end @@ -140,23 +140,23 @@ def paginated_collection(*args) end it "should use 'post' as the collection name when there is no I18n translation" do - expect(pagination.find_by_class('pagination_information').first.content).to eq "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 allow(I18n).to receive(:translate) { "Singular" } - expect(pagination.find_by_class('pagination_information').first.content).to eq "Displaying 1 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 it "should use 'posts' as the collection name when there is no I18n translation" do - expect(pagination.find_by_class('pagination_information').first.content).to eq "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 allow(I18n).to receive(:translate) { "Plural" } - expect(pagination.find_by_class('pagination_information').first.content).to eq "Displaying all 3 Plural" + expect(pagination.find_by_class("pagination-information").first.content).to eq "Showing all 3" end end @@ -167,30 +167,30 @@ def paginated_collection(*args) end it "should display 'No entries found'" do - expect(pagination.find_by_class('pagination_information').first.content).to eq "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 it "should display proper message (including number and not hash)" do - expect(pagination.find_by_class('pagination_information').first.content).to eq "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 it "should display proper message (including number and not hash)" do - expect(pagination.find_by_class('pagination_information').first.content.gsub(' ',' ')). - to eq "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 @@ -200,8 +200,8 @@ def paginated_collection(*args) end it "should show the proper item counts" do - expect(pagination.find_by_class('pagination_information').first.content.gsub(' ',' ')). - to eq "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 @@ -212,11 +212,10 @@ def paginated_collection(*args) describe "set to false" do it "should not show the total item counts" do - expect(collection).not_to receive(:num_pages) 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 "Displaying posts 1 - 30" + info = pagination.find_by_class("pagination-information").first.content.gsub(" ", " ") + expect(info).to eq "Showing 1-30" end end @@ -224,27 +223,81 @@ def paginated_collection(*args) 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 "Displaying posts 1 - 30 of 256 in total" + 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 = 10.times.map { Post.new } + 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("pagination_per_page").first } + 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_html.content).to match(/Per page/) expect(pagination_node).to have_css("select option", count: 3) end - 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 a33fc5cb749..ff9d101e3f6 100644 --- a/spec/unit/views/components/panel_spec.rb +++ b/spec/unit/views/components/panel_spec.rb @@ -1,10 +1,10 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Views::Panel do +RSpec.describe ActiveAdmin::Views::Panel do let(:arbre_panel) do render_arbre_component do panel "My Title" do - header_action link_to("My Link", "https://www.github.com/activeadmin/activeadmin") span("Hello World") end end @@ -13,21 +13,15 @@ let(:panel_html) { Capybara.string(arbre_panel.to_s) } it "should have a title h3" do - expect(panel_html).to have_css 'h3', text: "My Title" - end - - it "should add panel actions to the panel header" do - link = panel_html.find('h3 > div.header_action a') - expect(link.text).to eq('My Link') - expect(link[:href]).to eq("https://www.github.com/activeadmin/activeadmin") + expect(panel_html).to have_css "h3", text: "My Title" end it "should have a contents div" do - expect(panel_html).to have_css 'div.panel_contents' + expect(panel_html).to have_css "div.panel-body" end it "should add children to the contents div" do - expect(panel_html).to have_css 'div.panel_contents > span', text: "Hello World" + expect(panel_html).to have_css "div.panel-body > span", text: "Hello World" end context "with html-safe title" do 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 1f7a84ef2b9..00000000000 --- a/spec/unit/views/components/sidebar_section_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::Views::SidebarSection do - - let(:options) { {} } - - let(:section) do - ActiveAdmin::SidebarSection.new("Help Section", options) 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 - expect(html.find_by_tag("h3").first.content).to eq "Help Section" - end - - it "should have the class of 'sidebar_section'" do - expect(html.class_list).to include("sidebar_section") - end - - it "should have an id based on the title" do - expect(html.id).to eq "help-section_sidebar_section" - end - - it "should have a contents div" do - expect(html.find_by_tag("div").first.class_list).to include("panel_contents") - end - - it "should add children to the contents div" do - expect(html.find_by_tag("span").first.parent).to eq html.find_by_tag("div").first - end - - context 'with a custom class attribute' do - let(:options) { { class: 'custom_class' } } - - it "should have 'custom_class' class" do - expect(html.class_list).to include("custom_class") - end - end - - context "with attributes_table for resource" do - let(:post) { Post.create!(title: "Testing.") } - let(:section) do - ActiveAdmin::SidebarSection.new("Summary", options) do - attributes_table do - row :title - end - end - end - let(:assigns) { { resource: post, section: section } } - let(:html) do - render_arbre_component assigns do - sidebar_section(assigns[:section]) - end - end - - it "should have table" do - expect(html.find_by_tag("th").first.content).to eq "Title" - expect(html.find_by_tag("td").first.content).to eq "Testing." - end - 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 fef2d0fd3cd..00000000000 --- a/spec/unit/views/components/site_title_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -require 'rails_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 = double site_title: "Hello World", - site_title_image: nil, - site_title_link: nil - - site_title = build_title(namespace) - expect(site_title.content).to eq "Hello World" - end - - it "renders the return value of a method when a symbol" do - expect(helpers).to receive(:hello_world).and_return("Hello World") - - namespace = double site_title: :hello_world, - site_title_image: nil, - site_title_link: nil - - site_title = build_title(namespace) - expect(site_title.content).to eq "Hello World" - end - - it "renders the return value of a proc" do - namespace = double site_title: proc{ "Hello World" }, - site_title_image: nil, - site_title_link: nil - - site_title = build_title(namespace) - expect(site_title.content).to eq "Hello World" - end - - end - - context "when an image" do - - it "renders the string when a string is passed in" do - expect(helpers).to receive(:image_tag). - with("an/image.png", alt: nil, id: "site_title_image"). - and_return ''.html_safe - - namespace = double site_title: nil, - site_title_image: "an/image.png", - site_title_link: nil - - site_title = build_title(namespace) - expect(site_title.content.strip).to eq '' - end - - end - - context "when a link is present" do - - it "renders the string when a string is passed in" do - namespace = double site_title: "Hello World", - site_title_image: nil, - site_title_link: "/" - - site_title = build_title(namespace) - expect(site_title.content).to eq '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 7dba5da93cf..c921912a29d 100644 --- a/spec/unit/views/components/status_tag_spec.rb +++ b/spec/unit/views/components/status_tag_spec.rb @@ -1,249 +1,310 @@ -require 'rails_helper' +# frozen_string_literal: true +require "rails_helper" + +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 ActiveAdmin::Views::StatusTag do + describe "#tag_name" do + subject { status_tag(nil).tag_name } + it { is_expected.to eq "span" } + end - describe "#status_tag" do + context "when status is 'completed'" do + subject { status_tag("completed") } - # 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 + 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 '#tag_name' do - subject { super().tag_name } - it { is_expected.to eq 'span' } + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "completed") } end + end - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('status_tag') } + 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 - context "when status is 'completed'" do - subject { status_tag('completed') } + describe "#content" do + subject { super().content } + it { is_expected.to eq "In Progress" } + end - describe '#tag_name' do - subject { super().tag_name } - it { is_expected.to eq 'span' } - end + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "in_progress") } + end + end - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('status_tag') } - end + context "when status is 'in_progress'" do + subject { status_tag("in_progress") } - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('completed') } - 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 'Completed' } - end + describe "#content" do + subject { super().content } + it { is_expected.to eq "In Progress" } end - context "when status is 'in_progress'" do - subject { status_tag('in_progress') } + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "in_progress") } + end + end - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('in_progress') } - end + context "when status is 'In progress'" do + subject { status_tag("In progress") } - describe '#content' do - subject { super().content } - it { is_expected.to eq 'In Progress' } - end + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } end - context "when status is 'In progress'" do - subject { status_tag('In progress') } - - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('in_progress') } - end + describe "#content" do + subject { super().content } + it { is_expected.to eq "In Progress" } + 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('') } + context "when status is an empty string" do + subject { status_tag("") } - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('status_tag') } - 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 '' } - end + describe "#content" do + subject { super().content } + it { is_expected.to eq "" } end - context "when status is 'false'" do - subject { status_tag('false') } + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "") } + end + end - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('status_tag') } - end + context "when status is 'true'" do + subject { status_tag("true") } - describe '#content' do - subject { super().content } - it { is_expected.to eq('No') } - end + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } end - context "when status is false" do - subject { status_tag(false) } + describe "#content" do + subject { super().content } + it { is_expected.to eq("Yes") } + end - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('status_tag') } - end + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "yes") } + end + end - describe '#content' do - subject { super().content } - it { is_expected.to eq('No') } - 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 - context "when status is nil" do - subject { status_tag(nil) } + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "yes") } + end + end - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('status_tag') } - end + context "when status is 'false'" do + subject { status_tag("false") } - describe '#content' do - subject { super().content } - it { is_expected.to eq('No') } - end + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } end - context "when status is 'Active' and type is :ok" do - subject { status_tag('Active', :ok) } + describe "#content" do + subject { super().content } + it { is_expected.to eq("No") } + end - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('status_tag') } - end + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "no") } + end + end - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('active') } - end + context "when status is false" do + subject { status_tag(false) } - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('ok') } - end + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } end - 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("No") } + end - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('status_tag') } - end + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "no") } + end + end - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('active') } - end + context "when status is nil" do + subject { status_tag(nil) } - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('ok') } - end + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } end - context "when status is 'Active' and label is 'on'" do - subject { status_tag('Active', label: 'on') } + describe "#content" do + subject { super().content } + it { is_expected.to eq("Unknown") } + end - describe '#content' do - subject { super().content } - it { is_expected.to eq 'on' } + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "unset") } + end + + 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 } - it { is_expected.to include('status_tag') } + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } end - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('active') } + describe "#content" do + subject { super().content } + it { is_expected.to eq("Unspecified") } end - describe '#class_list' do - subject { super().class_list } - it { is_expected.not_to include('on') } + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "unset") } end 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 '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 + describe "#content" do + subject { super().content } + it { is_expected.to eq "Active" } + end - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('status_tag') } - end + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag", "ok") } + end - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('ok') } - end + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "active") } + end + end - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('so_useless') } - end + context "when status is 'Active' and label is 'on'" do + subject { status_tag("Active", label: "on") } - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('woot') } - end + describe "#content" do + subject { super().content } + it { is_expected.to eq "on" } + end - describe '#class_list' do - subject { super().class_list } - it { is_expected.to include('awesome') } - end + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } + end - describe '#id' do - subject { super().id } - it { is_expected.to eq 'useless' } - end + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "active") } end + end - context "when status is set to a Fixnum" do - subject { status_tag(42) } + 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") } - describe '#content' do - subject { super().content } - it { is_expected.to eq '42' } - end + describe "#content" do + subject { super().content } + it { is_expected.to eq "So Useless" } + end - describe '#class_list' do - subject { super().class_list } - it { is_expected.not_to include('42') } - end + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag", "woot", "awesome") } + end + + describe "#id" do + subject { super().id } + it { is_expected.to eq "useless" } + end + + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "so_useless") } + end + end + + context "when status is set to a Fixnum" do + subject { status_tag(42) } + + describe "#content" do + subject { super().content } + it { is_expected.to eq "42" } + end + + 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 31710027063..e7da7a76bc4 100644 --- a/spec/unit/views/components/table_for_spec.rb +++ b/spec/unit/views/components/table_for_spec.rb @@ -1,14 +1,18 @@ -require 'rails_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", starred: true), Post.new(title: "Second Post"), Post.new(title: "Third Post", starred: false)] + [ + 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 } + let(:assigns) { { collection: collection } } + let(:helpers) { mock_action_view } context "when creating a column using symbol argument" do let(:table) do @@ -44,9 +48,9 @@ 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 - expect(table.find_by_tag("th").first.class_list.to_a.join(' ')).to eq "col col-title" - expect(table.find_by_tag("th").last.class_list.to_a.join(' ')).to eq "col col-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 @@ -57,9 +61,9 @@ 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 - expect(table.find_by_tag("td").first.class_list.to_a.join(' ')).to eq "col col-title" - expect(table.find_by_tag("td").last.class_list.to_a.join(' ')).to eq "col col-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 @@ -77,9 +81,9 @@ 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 - expect(table.find_by_tag("th").first.class_list.to_a.join(' ')).to eq "col col-title" - expect(table.find_by_tag("th").last.class_list.to_a.join(' ')).to eq "col col-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 @@ -90,9 +94,9 @@ 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 - expect(table.find_by_tag("td").first.class_list.to_a.join(' ')).to eq "col col-title" - expect(table.find_by_tag("td").last.class_list.to_a.join(' ')).to eq "col col-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 @@ -135,9 +139,9 @@ 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 - expect(table.find_by_tag("th").first.class_list.to_a.join(' ')).to eq "col col-title" - expect(table.find_by_tag("th").last.class_list.to_a.join(' ')).to eq "col col-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 @@ -148,9 +152,9 @@ 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 - expect(table.find_by_tag("td").first.class_list.to_a.join(' ')).to eq "col col-title" - expect(table.find_by_tag("td").last.class_list.to_a.join(' ')).to eq "col col-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 @@ -165,13 +169,14 @@ end end - it "should add a class to each table header based on the col name" do - expect(table.find_by_tag("th").first.class_list).to include("col-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 expect(table.find_by_tag("td")[index].content.strip).to eq content end @@ -197,26 +202,30 @@ 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 table header based on class option or the col name" do - expect(table.find_by_tag("th").first.class_list.to_a.join(' ')).to eq "col col-my_custom_title" - expect(table.find_by_tag("th").last.class_list.to_a.join(' ')).to eq "col datetime" + 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 class to each cell based on class option or the col name" do - expect(table.find_by_tag("td").first.class_list.to_a.join(' ')).to eq "col col-my_custom_title" - expect(table.find_by_tag("td").last.class_list.to_a.join(' ')).to eq "col datetime" + 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 @@ -228,6 +237,7 @@ end end end + it "should render" do expect(table.find_by_tag("th").first.content).to eq "Title" end @@ -242,6 +252,7 @@ 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" @@ -253,11 +264,12 @@ context "when using an Array of Hashes" do let(:table) do render_arbre_component nil, helpers do - table_for [{foo: 1},{foo: 2}] do + table_for [{ foo: 1 }, { foo: 2 }] do column :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" @@ -275,36 +287,82 @@ end it "should render boolean attribute within status tag" do - expect(table.find_by_tag("span").first.class_list.to_a.join(' ')).to eq "status_tag yes" + expect(table.find_by_tag("span").first.class_list.to_s).to eq "status-tag" expect(table.find_by_tag("span").first.content).to eq "Yes" - expect(table.find_by_tag("span").last.class_list.to_a.join(' ')).to eq "status_tag no" + expect(table.find_by_tag("span").last.class_list.to_s).to eq "status-tag" expect(table.find_by_tag("span").last.content).to eq "No" end end - context 'when row_class' do + context "with tbody_html option" do + let(:table) do + render_arbre_component assigns, helpers do + table_for(collection, tbody_html: { class: "my-class", data: { size: collection.size } }) do + column :starred + end + end + end + + it "should render data-size attribute within tbody tag" do + tbody = table.find_by_tag("tbody").first + expect(tbody.attributes).to include( + class: "my-class", + data: { size: 3 }) + end + end + + context "with row_class (soft deprecated)" do + let(:table) do + render_arbre_component assigns, helpers do + table_for(collection, row_class: -> 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 { 'starred' if e.starred }) 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 render boolean attribute within status tag' do - trs = table.find_by_tag('tr') + it "should render html attributes within collection row" do + trs = table.find_by_tag("tr") expect(trs.size).to eq 4 - expect(trs.first.class_list.to_a.join(' ')).to eq '' - expect(trs.second.class_list.to_a.join(' ')).to eq 'odd starred' - expect(trs.third.class_list.to_a.join(' ')).to eq 'even' - expect(trs.fourth.class_list.to_a.join(' ')).to eq 'odd' + 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 context "when i18n option is specified" do - before(:each) do - I18n.backend.store_translations :en, - activerecord: { attributes: { post: { title: "Name" } } } + around do |example| + with_translation %i[activerecord attributes post title], "Name" do + example.call + end end let(:table) do @@ -321,16 +379,19 @@ end context "when i18n option is not specified" do - before(:each) do - I18n.backend.store_translations :en, - activerecord: { attributes: { post: { title: "Name" } } } + 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.create( + [ + { title: "First Post", starred: true }, + { title: "Second Post" }, + ] + ) Post.where(starred: true) end @@ -349,7 +410,6 @@ end describe "column sorting" do - def build_column(*args, &block) ActiveAdmin::Views::TableFor::Column.new(*args, &block) end @@ -357,72 +417,77 @@ def build_column(*args, &block) subject { table_column } context "when default" do - let(:table_column){ build_column(:username) } + let(:table_column) { build_column(:username) } it { is_expected.to be_sortable } - describe '#sort_key' do + describe "#sort_key" do subject { super().sort_key } - it{ is_expected.to eq("username") } + it { is_expected.to eq("username") } end end context "when a block given with no sort key" do - let(:table_column){ build_column("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 a sort key" do - let(:table_column){ build_column("Username", sortable: :username){ } } + let(:table_column) { build_column("Username", sortable: :username) {} } it { is_expected.to be_sortable } - describe '#sort_key' do + describe "#sort_key" do subject { super().sort_key } - it{ is_expected.to eq("username") } + 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) { } } + 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 symbol given as a data column should be sortable' do - let(:table_column){ build_column('Username column', :username) } + 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 + describe "#sort_key" do subject { super().sort_key } - it { is_expected.to eq 'username' } + it { is_expected.to eq "username" } end end - context 'when sortable: true with a symbol and string' do - let(:table_column){ build_column('Username column', :username, sortable: true) } + 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 + describe "#sort_key" do subject { super().sort_key } - it { is_expected.to eq 'username' } + it { is_expected.to eq "username" } end end context "when sortable: false with a symbol" do - let(:table_column){ build_column(:username, sortable: false) } + 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) } + 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) } + 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) { } } + 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 diff --git a/spec/unit/views/components/tabs_spec.rb b/spec/unit/views/components/tabs_spec.rb deleted file mode 100644 index 8329412ca66..00000000000 --- a/spec/unit/views/components/tabs_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::Views::Tabs do - describe "creating with the dsl" do - context "when creating tabs with a symbol" do - let(:tabs) do - render_arbre_component do - tabs do - tab :overview - end - end - end - - it "should create a tab navigation bar based on the symbol" do - expect(tabs.find_by_tag('li').first.content).to include "Overview" - end - end - - context "when creating a tab with a block" do - let(:tabs) do - render_arbre_component do - tabs do - tab :overview do - span 'tab 1' - end - end - end - end - - it "should create a tab navigation bar based on the symbol" do - expect(tabs.find_by_tag('li').first.content).to include "Overview" - end - - it "should create a tab with a span inside of it" do - expect(tabs.find_by_tag('span').first.content).to eq('tab 1') - end - end - end -end diff --git a/spec/unit/views/components/unsupported_browser_spec.rb b/spec/unit/views/components/unsupported_browser_spec.rb deleted file mode 100644 index bd259573e64..00000000000 --- a/spec/unit/views/components/unsupported_browser_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::Views::UnsupportedBrowser do - - let(:helpers){ mock_action_view } - let(:namespace) { double :namespace, unsupported_browser_matcher: /MSIE [1-8]\.0/ } - let(:component) { double :unsupported_browser_component } - let(:view_factory) { double :view_factory, unsupported_browser: component } - let(:base) { ActiveAdmin::Views::Pages::Base.new } - - def build_panel - render_arbre_component({}, helpers) do - insert_tag ActiveAdmin::Views::UnsupportedBrowser - end - end - - it "should render the panel" do - expect(I18n).to receive(:t).and_return("headline", "recommendation" ,"turn_off_compatibility_view") - expect(build_panel.content.gsub(/\s+/, "")).to eq "

    headline

    recommendation

    turn_off_compatibility_view

    " - end - - describe "ActiveAdmin::Views::Pages::Base behavior" do - context "when the reqex match" do - - it "should build the unsupported browser panel" do - expect(base).to receive(:active_admin_namespace).and_return(namespace) - expect(base).to receive(:env).and_return({ "HTTP_USER_AGENT" => "Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.2; Trident/6.0)" }) - expect(base).to receive(:view_factory).and_return(view_factory) - expect(base).to receive(:insert_tag).with(component) - base.send(:build_unsupported_browser) - end - - end - - context "when the regex not match" do - - it "should not build the unsupported browser panel" do - expect(base).to receive(:active_admin_namespace).and_return(namespace) - expect(base).to receive(:env).and_return({ "HTTP_USER_AGENT" => "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)" }) - expect(base).to receive(:insert_tag).never - base.send(:build_unsupported_browser) - end - - end - end - -end diff --git a/spec/unit/views/index_as_blog_spec.rb b/spec/unit/views/index_as_blog_spec.rb deleted file mode 100644 index 6a001f6397c..00000000000 --- a/spec/unit/views/index_as_blog_spec.rb +++ /dev/null @@ -1,76 +0,0 @@ -require 'rails_helper' - -include ActiveAdmin -describe ActiveAdmin::Views::IndexAsBlog do - subject { described_class.new } - - describe '#build' do - let(:page_presenter) { double('page_presenter', block: nil) } - let(:collection) { double('collection') } - - before do - expect(subject).to receive('build_posts') - expect(subject).to receive('add_class').with('index') - end - - context 'when page_presenter has no block' do - before do - subject.build(page_presenter, collection) - end - - it do - expect(subject.instance_variable_get(:@page_presenter)) - .to eq(page_presenter) - expect(subject.instance_variable_get(:@collection)).to eq(collection) - end - end - - context 'when page_presenter has block' do - let(:block) { Proc.new { double('proc_method') } } - - before do - allow(page_presenter).to receive(:block).and_return(block) - allow(subject).to receive('instance_exec') - subject.build(page_presenter, collection) - end - - it do - expect(subject).to have_received('instance_exec') - end - end - end - - %w(title body).each do |method| - describe "#{method}" do - context 'when block given' do - let(:block_result) { double('block_result') } - - it "should use the block to set the #{method}" do - expect( - subject.public_send("#{method}") do - block_result - end.yield - ).to eq(block_result) - end - end - - context 'when no block and method given' do - let(:method) { double('method') } - - it "should use method to set the #{method}" do - expect(subject.public_send("#{method}", method)).to eq(method) - end - end - - context 'when no block and no method given' do - it 'should be nil' do - expect(subject.public_send("#{method}")).to eq(nil) - end - end - end - end - - describe '.index_name' do - it { expect(described_class.index_name).to eq('blog') } - end -end diff --git a/spec/unit/views/pages/form_spec.rb b/spec/unit/views/pages/form_spec.rb deleted file mode 100644 index 3201428d25b..00000000000 --- a/spec/unit/views/pages/form_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::Views::Pages::Form do - describe "#title" do - let!(:application){ ActiveAdmin::Application.new } - let(:namespace){ ActiveAdmin::Namespace.new(application, "Admin") } - let!(:params){ { controller: "UsersController", action: "edit" } } - let(:helpers) do - helpers = mock_action_view - allow(helpers).to receive(:active_admin_config).and_return(namespace.register(Post)) - allow(helpers).to receive(:params).and_return(params) - helpers - end - - let(:arbre_context) do - OpenStruct.new(params: params, helpers: helpers, assigns: {}) - end - - context "when page_title is assigned" do - it "should show the set page title" do - arbre_context.assigns[:page_title] = "My Page Title" - page = ActiveAdmin::Views::Pages::Form.new(arbre_context) - expect(page.title).to eq "My Page Title" - end - end - - context "when page_title is not assigned" do - { - "new" => "New Post", - "create" => "New Post", - "edit" => "Edit Post", - "update" => "Edit Post" - }.each do |action, title| - it "should show the correct I18n text on the #{action} action" do - params[:action] = action - page = ActiveAdmin::Views::Pages::Form.new(arbre_context) - expect(page.title).to eq title - end - end - end - end -end diff --git a/spec/unit/views/pages/index_spec.rb b/spec/unit/views/pages/index_spec.rb deleted file mode 100644 index 041344ae545..00000000000 --- a/spec/unit/views/pages/index_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::Views::Pages::Index do - describe "#title" do - let!(:application){ ActiveAdmin::Application.new } - let(:namespace){ ActiveAdmin::Namespace.new(application, "Admin") } - let!(:params){ { controller: "UsersController", action: "edit" } } - let(:helpers) do - helpers = mock_action_view - allow(helpers).to receive(:active_admin_config).and_return(namespace.register(Post)) - allow(helpers).to receive(:params).and_return(params) - helpers - end - - let(:arbre_context) do - OpenStruct.new(params: params, helpers: helpers, assigns: {}) - end - - context "when config[:title] is assigned" do - context "with a Proc" do - it "should return the value of the assigned Proc" do - page = ActiveAdmin::Views::Pages::Index.new(arbre_context) - allow(page).to receive(:config).and_return(title: ->{ "My Page Title" }) - expect(page.title).to eq "My Page Title" - end - end - - context "with a String" do - it "should return the assigned String" do - page = ActiveAdmin::Views::Pages::Index.new(arbre_context) - allow(page).to receive(:config).and_return(title: ->{ "My Page Title" }) - expect(page.title).to eq "My Page Title" - end - end - - context "with a Integer" do - it "should return the Integer" do - page = ActiveAdmin::Views::Pages::Index.new(arbre_context) - allow(page).to receive(:config).and_return(title: 1) - expect(page.title).to eq 1 - end - end - end - - context "when page_title is assigned" do - it "should return the set page title" do - arbre_context.assigns[:page_title] = "My Page Title" - page = ActiveAdmin::Views::Pages::Index.new(arbre_context) - expect(page.title).to eq "My Page Title" - end - end - - context "when page_title is not assigned" do - it "should return the correct I18n text" do - page = ActiveAdmin::Views::Pages::Index.new(arbre_context) - expect(page.title).to eq "Posts" - end - 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 1b742d61a2e..00000000000 --- a/spec/unit/views/pages/layout_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::Views::Pages::Layout do - - let(:assigns){ {} } - let(:helpers) do - helpers = mock_action_view - - { active_admin_application: active_admin_application, - active_admin_config: double('Config', action_items?: nil, breadcrumb: nil, sidebar_sections?: nil), - active_admin_namespace: active_admin_namespace, - csrf_meta_tag: '', - current_active_admin_user: nil, - current_active_admin_user?: false, - current_menu: double('Menu', items: []), - params: {controller: 'UsersController', action: 'edit'}, - env: {} - }.each do |method, returns| - allow(helpers).to receive(method).and_return returns - end - - 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" - - expect(layout.title).to eq "My Page Title" - end - - it "should be the default translation" do - helpers.params[:action] = "edit" - - expect(layout.title).to eq "Edit" - end - - describe "the body" do - - it "should have class 'active_admin'" do - expect(layout.build.class_list).to include 'active_admin' - end - - it "should have namespace class" do - expect(layout.build.class_list).to 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 a55ba118eb7..00000000000 --- a/spec/unit/views/pages/show_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'rails_helper' - -describe ActiveAdmin::Views::Pages::Show do - - describe "the resource" do - let(:helpers) { double resource: resource } - let(:arbre_context) { Arbre::Context.new({}, helpers) } - subject(:page) { ActiveAdmin::Views::Pages::Show.new(arbre_context) } - - context 'when the resource does not respond to #decorator' do - let(:resource) { 'Test Resource' } - - it "normally returns the resource" do - expect(page.resource).to eq 'Test Resource' - end - end - - context 'when you pass a block to main content' do - let(:block) { lambda { } } - let(:resource_class) { double(columns: [double('column', name: 'field')]) } - let(:resource) { double('resource', class: resource_class) } - - before { allow(page).to receive(:active_admin_config).and_return(double(comments?: false))} - - it 'appends it to the output' do - expect(page).to receive(:attributes_table).with(:field).and_yield - page.default_main_content(&block) - 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 e52556fbd08..00000000000 --- a/spec/unit/views/tabbed_navigation_spec.rb +++ /dev/null @@ -1,158 +0,0 @@ -require 'rails_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) { Capybara.string(tabbed_navigation.to_s) } - - before do - allow(helpers).to receive(: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 - - menu.add label: "Charles Smith", id: "current_user", url: -> { nil } - end - - it "should generate a ul" do - expect(html).to have_selector("ul") - end - - it "should generate an li for each item" do - expect(html).to have_selector("ul > li") - end - - it "should generate a link for each item" do - expect(html).to have_selector("a[href='/admin/posts']", text: "Blog Posts") - end - - it "should generate a nested list for children" do - expect(html).to have_selector("li > ul") - end - - it "should generate a nested list with li for each child" do - expect(html).to have_selector("ul > li#a_sub_reports") - expect(html).to have_selector("ul > li#b_sub_reports") - end - - it "should generate a valid id from a label proc" do - expect(html).to have_selector("ul > li#label_proc_sub_reports") - end - - it "should not generate a link for user administration" do - expect(html).to_not have_selector("a[href='/admin/user-administration']", text: "User administration") - end - - it "should generate the administration parent menu" do - expect(html).to have_selector("a[href='/admin/administration']", text: "Administration") - end - - it "should not generate a link for order management" do - expect(html).to_not have_selector("a[href='/admin/order-management']", text: "Order management") - end - - it "should not generate a link for bill management" do - expect(html).to_not have_selector("a[href='/admin/bill-management']", text: "Bill management") - end - - it "should not generate the management parent menu" do - expect(html).to_not have_selector("a[href='#']", text: "Management") - end - - context "when url is nil" do - it "should generate a span" do - selector = "li#current_user > span" - expect(html).to have_selector(selector, text: "Charles Smith") - end - end - - describe "marking current item" do - - it "should add the 'current' class to the li" do - assigns[:current_tab] = menu["Blog Posts"] - expect(html).to have_selector("li.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"] - expect(html).to have_selector("li#reports.current") - expect(html).to have_selector("li#reports.has_nested") - expect(html).to have_selector("li#a_sub_reports.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: "/" - expect(tabbed_navigation.menu_items).to eq 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 } - expect(tabbed_navigation.menu_items).to 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? - expect(tabbed_navigation.menu_items).to 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 - expect(tabbed_navigation.menu_items).to 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 - expect(tabbed_navigation.menu_items.size).to eq(1) - 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..6c7f72970bb --- /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 `yarn install`." + end +end diff --git a/tasks/docs.rake b/tasks/docs.rake deleted file mode 100644 index cbfc62055a3..00000000000 --- a/tasks/docs.rake +++ /dev/null @@ -1,37 +0,0 @@ -namespace :docs do - - AUTOGEN_WARNING = <<-EOD - - -EOD - - def filename_from_module(mod) - mod.name.to_s.underscore.tr('_', '-') - end - - def write_docstrings_to(path, mods) - mods.each do |mod| - File.open("#{path}/#{filename_from_module(mod)}.md", 'w+') do |f| - f << AUTOGEN_WARNING + mod.docstring + "\n" - 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/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 78a8a5ade3b..00000000000 --- a/tasks/parallel_tests.rake +++ /dev/null @@ -1,66 +0,0 @@ -require 'parallel' -require 'shellwords' - -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 true - end - end - - def run_in_parallel(command) - bash("ENV['TEST_ENV_NUMBER']=#{Parallel.processor_count} #{command}") - end - - def bash(command) - escaped_command = Shellwords.escape(command) - system("bash -c #{escaped_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 request).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 906e00fb62b..8926ce592d0 100644 --- a/tasks/test.rake +++ b/tasks/test.rake @@ -1,83 +1,76 @@ -desc "Creates a test rails app for the specs to run against" -task :setup, :parallel do |t, args| - require 'rails/version' - if File.exists? dir = "spec/rails/rails-#{Rails::VERSION::STRING}" - puts "test app #{dir} already exists; skipping" - else - system("mkdir spec/rails") unless File.exists?("spec/rails") - system "#{'INSTALL_PARALLEL=yes' if args[:parallel]} bundle exec rails new #{dir} -m spec/support/rails_template.rb --skip-bundle" - Rake::Task['parallel:after_setup_hook'].invoke if args[:parallel] - end -end - -desc "Run the full suite using 1 core" -task test: ['spec:unit', 'spec:request', 'cucumber', 'cucumber:class_reloading'] +# frozen_string_literal: true +desc "Run the full suite using parallel_tests to run on multiple cores" +task test: [:setup, :spec, :cucumber] -require 'coveralls/rake/task' -Coveralls::RakeTask.new -task test_with_coveralls: [:test, 'coveralls:push'] +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 "simplecov" - cmd "./script/use_rails #{version}" - cmd "bundle exec rspec spec" - cmd "bundle exec cucumber features" - cmd "bundle exec cucumber -p class-reloading features" + SimpleCov.command_name "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 request specs" - RSpec::Core::RakeTask.new(:request) do |t| - t.pattern = "spec/requests/**/*_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..caf1b975c8a --- /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("yarn 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("yarn add @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("yarn install") + Kernel.system("yarn 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 122e603db08..00000000000 --- a/tasks/yard.rake +++ /dev/null @@ -1,9 +0,0 @@ -if Gem.loaded_specs['yard'] - require 'yard' - require 'yard/rake/yardoc_task' - - YARD::Rake::YardocTask.new do |t| - t.after = ->{ Rake::Task['docs:build'].invoke } - t.files = ['lib/**/*.rb'] - end -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}; diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000000..600fd1b53d6 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,2142 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@algolia/abtesting@1.13.0": + version "1.13.0" + resolved "https://registry.yarnpkg.com/@algolia/abtesting/-/abtesting-1.13.0.tgz#88bbf09c5846fbe9a213461ea8bfdcf756c060dd" + integrity sha512-Zrqam12iorp3FjiKMXSTpedGYznZ3hTEOAr2oCxI8tbF8bS1kQHClyDYNq/eV0ewMNLyFkgZVWjaS+8spsOYiQ== + dependencies: + "@algolia/client-common" "5.47.0" + "@algolia/requester-browser-xhr" "5.47.0" + "@algolia/requester-fetch" "5.47.0" + "@algolia/requester-node-http" "5.47.0" + +"@algolia/autocomplete-core@1.17.7": + version "1.17.7" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz#2c410baa94a47c5c5f56ed712bb4a00ebe24088b" + integrity sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q== + dependencies: + "@algolia/autocomplete-plugin-algolia-insights" "1.17.7" + "@algolia/autocomplete-shared" "1.17.7" + +"@algolia/autocomplete-plugin-algolia-insights@1.17.7": + version "1.17.7" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz#7d2b105f84e7dd8f0370aa4c4ab3b704e6760d82" + integrity sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A== + dependencies: + "@algolia/autocomplete-shared" "1.17.7" + +"@algolia/autocomplete-preset-algolia@1.17.7": + version "1.17.7" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz#c9badc0d73d62db5bf565d839d94ec0034680ae9" + integrity sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA== + dependencies: + "@algolia/autocomplete-shared" "1.17.7" + +"@algolia/autocomplete-shared@1.17.7": + version "1.17.7" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz#105e84ad9d1a31d3fb86ba20dc890eefe1a313a0" + integrity sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg== + +"@algolia/client-abtesting@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@algolia/client-abtesting/-/client-abtesting-5.47.0.tgz#0d64f45844dcf18cebe9d1dae183052ec3d3f6cb" + integrity sha512-aOpsdlgS9xTEvz47+nXmw8m0NtUiQbvGWNuSEb7fA46iPL5FxOmOUZkh8PREBJpZ0/H8fclSc7BMJCVr+Dn72w== + dependencies: + "@algolia/client-common" "5.47.0" + "@algolia/requester-browser-xhr" "5.47.0" + "@algolia/requester-fetch" "5.47.0" + "@algolia/requester-node-http" "5.47.0" + +"@algolia/client-analytics@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-5.47.0.tgz#239ce66964a766316ec2e3a6a1773dfa89f13203" + integrity sha512-EcF4w7IvIk1sowrO7Pdy4Ako7x/S8+nuCgdk6En+u5jsaNQM4rTT09zjBPA+WQphXkA2mLrsMwge96rf6i7Mow== + dependencies: + "@algolia/client-common" "5.47.0" + "@algolia/requester-browser-xhr" "5.47.0" + "@algolia/requester-fetch" "5.47.0" + "@algolia/requester-node-http" "5.47.0" + +"@algolia/client-common@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.47.0.tgz#de05deb8c058e9f125eea51de317ab1406247642" + integrity sha512-Wzg5Me2FqgRDj0lFuPWFK05UOWccSMsIBL2YqmTmaOzxVlLZ+oUqvKbsUSOE5ud8Fo1JU7JyiLmEXBtgDKzTwg== + +"@algolia/client-insights@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@algolia/client-insights/-/client-insights-5.47.0.tgz#9adbc82f6bfa896266559843fc74eddaaf9bff15" + integrity sha512-Ci+cn/FDIsDxSKMRBEiyKrqybblbk8xugo6ujDN1GSTv9RIZxwxqZYuHfdLnLEwLlX7GB8pqVyqrUSlRnR+sJA== + dependencies: + "@algolia/client-common" "5.47.0" + "@algolia/requester-browser-xhr" "5.47.0" + "@algolia/requester-fetch" "5.47.0" + "@algolia/requester-node-http" "5.47.0" + +"@algolia/client-personalization@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-5.47.0.tgz#f45bca61625458a2c26d0f241bbc249f47aa0fe1" + integrity sha512-gsLnHPZmWcX0T3IigkDL2imCNtsQ7dR5xfnwiFsb+uTHCuYQt+IwSNjsd8tok6HLGLzZrliSaXtB5mfGBtYZvQ== + dependencies: + "@algolia/client-common" "5.47.0" + "@algolia/requester-browser-xhr" "5.47.0" + "@algolia/requester-fetch" "5.47.0" + "@algolia/requester-node-http" "5.47.0" + +"@algolia/client-query-suggestions@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@algolia/client-query-suggestions/-/client-query-suggestions-5.47.0.tgz#20af440297524956aff483dccef7222c57366f04" + integrity sha512-PDOw0s8WSlR2fWFjPQldEpmm/gAoUgLigvC3k/jCSi/DzigdGX6RdC0Gh1RR1P8Cbk5KOWYDuL3TNzdYwkfDyA== + dependencies: + "@algolia/client-common" "5.47.0" + "@algolia/requester-browser-xhr" "5.47.0" + "@algolia/requester-fetch" "5.47.0" + "@algolia/requester-node-http" "5.47.0" + +"@algolia/client-search@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.47.0.tgz#b74b948adbd2908cb70a73a17e8a23d7e5ae439c" + integrity sha512-b5hlU69CuhnS2Rqgsz7uSW0t4VqrLMLTPbUpEl0QVz56rsSwr1Sugyogrjb493sWDA+XU1FU5m9eB8uH7MoI0g== + dependencies: + "@algolia/client-common" "5.47.0" + "@algolia/requester-browser-xhr" "5.47.0" + "@algolia/requester-fetch" "5.47.0" + "@algolia/requester-node-http" "5.47.0" + +"@algolia/ingestion@1.47.0": + version "1.47.0" + resolved "https://registry.yarnpkg.com/@algolia/ingestion/-/ingestion-1.47.0.tgz#47ffeba5120a4b186a81a2adb1ffda2902c6f3f1" + integrity sha512-WvwwXp5+LqIGISK3zHRApLT1xkuEk320/EGeD7uYy+K8WwDd5OjXnhjuXRhYr1685KnkvWkq1rQ/ihCJjOfHpQ== + dependencies: + "@algolia/client-common" "5.47.0" + "@algolia/requester-browser-xhr" "5.47.0" + "@algolia/requester-fetch" "5.47.0" + "@algolia/requester-node-http" "5.47.0" + +"@algolia/monitoring@1.47.0": + version "1.47.0" + resolved "https://registry.yarnpkg.com/@algolia/monitoring/-/monitoring-1.47.0.tgz#3ea90f176495a1130887b0780d91ff91fd42b9f9" + integrity sha512-j2EUFKAlzM0TE4GRfkDE3IDfkVeJdcbBANWzK16Tb3RHz87WuDfQ9oeEW6XiRE1/bEkq2xf4MvZesvSeQrZRDA== + dependencies: + "@algolia/client-common" "5.47.0" + "@algolia/requester-browser-xhr" "5.47.0" + "@algolia/requester-fetch" "5.47.0" + "@algolia/requester-node-http" "5.47.0" + +"@algolia/recommend@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-5.47.0.tgz#71a759cdbe67e27f2299ab87d3c8aa4392001a88" + integrity sha512-+kTSE4aQ1ARj2feXyN+DMq0CIDHJwZw1kpxIunedkmpWUg8k3TzFwWsMCzJVkF2nu1UcFbl7xsIURz3Q3XwOXA== + dependencies: + "@algolia/client-common" "5.47.0" + "@algolia/requester-browser-xhr" "5.47.0" + "@algolia/requester-fetch" "5.47.0" + "@algolia/requester-node-http" "5.47.0" + +"@algolia/requester-browser-xhr@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.47.0.tgz#0e76a8e3db0b09235178cb47c9de0a198db7b52d" + integrity sha512-Ja+zPoeSA2SDowPwCNRbm5Q2mzDvVV8oqxCQ4m6SNmbKmPlCfe30zPfrt9ho3kBHnsg37pGucwOedRIOIklCHw== + dependencies: + "@algolia/client-common" "5.47.0" + +"@algolia/requester-fetch@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.47.0.tgz#de68b44bef30d03919be249a6ff30e82975494a9" + integrity sha512-N6nOvLbaR4Ge+oVm7T4W/ea1PqcSbsHR4O58FJ31XtZjFPtOyxmnhgCmGCzP9hsJI6+x0yxJjkW5BMK/XI8OvA== + dependencies: + "@algolia/client-common" "5.47.0" + +"@algolia/requester-node-http@5.47.0": + version "5.47.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.47.0.tgz#b8e46c1e80b74d9146dec7b0219131198951198a" + integrity sha512-z1oyLq5/UVkohVXNDEY70mJbT/sv/t6HYtCvCwNrOri6pxBJDomP9R83KOlwcat+xqBQEdJHjbrPh36f1avmZA== + dependencies: + "@algolia/client-common" "5.47.0" + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + +"@babel/parser@^7.28.5": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd" + integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ== + dependencies: + "@babel/types" "^7.28.6" + +"@babel/types@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.6.tgz#c3e9377f1b155005bcc4c46020e7e394e13089df" + integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + +"@docsearch/css@3.8.2": + version "3.8.2" + resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.8.2.tgz#7973ceb6892c30f154ba254cd05c562257a44977" + integrity sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ== + +"@docsearch/js@3.8.2": + version "3.8.2" + resolved "https://registry.yarnpkg.com/@docsearch/js/-/js-3.8.2.tgz#bdcfc9837700eb38453b88e211ab5cc5a3813cc6" + integrity sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ== + dependencies: + "@docsearch/react" "3.8.2" + preact "^10.0.0" + +"@docsearch/react@3.8.2": + version "3.8.2" + resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.8.2.tgz#7b11d39b61c976c0aa9fbde66e6b73b30f3acd42" + integrity sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg== + dependencies: + "@algolia/autocomplete-core" "1.17.7" + "@algolia/autocomplete-preset-algolia" "1.17.7" + "@docsearch/css" "3.8.2" + algoliasearch "^5.14.2" + +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + +"@eslint-community/eslint-utils@^4.8.0": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" + integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.12.1": + version "4.12.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== + +"@eslint/config-array@^0.21.1": + version "0.21.1" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.1.tgz#7d1b0060fea407f8301e932492ba8c18aff29713" + integrity sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA== + dependencies: + "@eslint/object-schema" "^2.1.7" + debug "^4.3.1" + minimatch "^3.1.2" + +"@eslint/config-helpers@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz#1bd006ceeb7e2e55b2b773ab318d300e1a66aeda" + integrity sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw== + dependencies: + "@eslint/core" "^0.17.0" + +"@eslint/core@^0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.17.0.tgz#77225820413d9617509da9342190a2019e78761c" + integrity sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ== + dependencies: + "@types/json-schema" "^7.0.15" + +"@eslint/eslintrc@^3.3.1": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.3.tgz#26393a0806501b5e2b6a43aa588a4d8df67880ac" + integrity sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^10.0.1" + globals "^14.0.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.1" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@9.39.2": + version "9.39.2" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.2.tgz#2d4b8ec4c3ea13c1b3748e0c97ecd766bdd80599" + integrity sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA== + +"@eslint/object-schema@^2.1.7": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.7.tgz#6e2126a1347e86a4dedf8706ec67ff8e107ebbad" + integrity sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA== + +"@eslint/plugin-kit@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz#9779e3fd9b7ee33571a57435cf4335a1794a6cb2" + integrity sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA== + dependencies: + "@eslint/core" "^0.17.0" + levn "^0.4.1" + +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.7" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.7.tgz#822cb7b3a12c5a240a24f621b5a2413e27a45f26" + integrity sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ== + dependencies: + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.4.0" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" + integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== + +"@iconify-json/simple-icons@^1.2.21": + version "1.2.67" + resolved "https://registry.yarnpkg.com/@iconify-json/simple-icons/-/simple-icons-1.2.67.tgz#067e707dce5b8bc7677627fb47c6bdb325f36ed8" + integrity sha512-RGJRwlxyup54L1UDAjCshy3ckX5zcvYIU74YLSnUgHGvqh6B4mvksbGNHAIEp7dZQ6cM13RZVT5KC07CmnFNew== + dependencies: + "@iconify/types" "*" + +"@iconify/types@*": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@iconify/types/-/types-2.0.0.tgz#ab0e9ea681d6c8a1214f30cd741fe3a20cc57f57" + integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg== + +"@jridgewell/sourcemap-codec@^1.5.5": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@popperjs/core@^2.9.3": + version "2.11.8" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + +"@rails/ujs@7.1.600": + version "7.1.600" + resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.1.600.tgz#781d2737f3a81da81e78d93ec779099871288965" + integrity sha512-ntMYJ8++n1zcIJ1g75or7Va9IU3ueCHgp9IJk7Ny7eFEbu0Kx40EHZ2FNhoxoUEjlF5NifCFCbvoK5rDK5HpIQ== + +"@rollup/plugin-alias@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-6.0.0.tgz#f7fa8c806db9c073baa6b00e4b1c396edef9beb2" + integrity sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g== + +"@rollup/plugin-node-resolve@^15.2.3": + version "15.3.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz#66008953c2524be786aa319d49e32f2128296a78" + integrity sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA== + dependencies: + "@rollup/pluginutils" "^5.0.1" + "@types/resolve" "1.20.2" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.22.1" + +"@rollup/pluginutils@^5.0.1": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz#57ba1b0cbda8e7a3c597a4853c807b156e21a7b4" + integrity sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^4.0.2" + +"@rollup/rollup-android-arm-eabi@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz#add5e608d4e7be55bc3ca3d962490b8b1890e088" + integrity sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg== + +"@rollup/rollup-android-arm64@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz#10bd0382b73592beee6e9800a69401a29da625c4" + integrity sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w== + +"@rollup/rollup-darwin-arm64@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz#1e99ab04c0b8c619dd7bbde725ba2b87b55bfd81" + integrity sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg== + +"@rollup/rollup-darwin-x64@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz#69e741aeb2839d2e8f0da2ce7a33d8bd23632423" + integrity sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w== + +"@rollup/rollup-freebsd-arm64@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz#3736c232a999c7bef7131355d83ebdf9651a0839" + integrity sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug== + +"@rollup/rollup-freebsd-x64@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz#227dcb8f466684070169942bd3998901c9bfc065" + integrity sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q== + +"@rollup/rollup-linux-arm-gnueabihf@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz#ba004b30df31b724f99ce66e7128248bea17cb0c" + integrity sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw== + +"@rollup/rollup-linux-arm-musleabihf@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz#6929f3e07be6b6da5991f63c6b68b3e473d0a65a" + integrity sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw== + +"@rollup/rollup-linux-arm64-gnu@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz#06e89fd4a25d21fe5575d60b6f913c0e65297bfa" + integrity sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g== + +"@rollup/rollup-linux-arm64-musl@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz#fddabf395b90990d5194038e6cd8c00156ed8ac0" + integrity sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q== + +"@rollup/rollup-linux-loong64-gnu@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz#04c10bb764bbf09a3c1bd90432e92f58d6603c36" + integrity sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA== + +"@rollup/rollup-linux-loong64-musl@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz#f2450361790de80581d8687ea19142d8a4de5c0f" + integrity sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw== + +"@rollup/rollup-linux-ppc64-gnu@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz#0474f4667259e407eee1a6d38e29041b708f6a30" + integrity sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w== + +"@rollup/rollup-linux-ppc64-musl@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz#9f32074819eeb1ddbe51f50ea9dcd61a6745ec33" + integrity sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw== + +"@rollup/rollup-linux-riscv64-gnu@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz#3fdb9d4b1e29fb6b6a6da9f15654d42eb77b99b2" + integrity sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A== + +"@rollup/rollup-linux-riscv64-musl@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz#1de780d64e6be0e3e8762035c22e0d8ea68df8ed" + integrity sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw== + +"@rollup/rollup-linux-s390x-gnu@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz#1da022ffd2d9e9f0fd8344ea49e113001fbcac64" + integrity sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg== + +"@rollup/rollup-linux-x64-gnu@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz#78c16eef9520bd10e1ea7a112593bb58e2842622" + integrity sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg== + +"@rollup/rollup-linux-x64-musl@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz#a7598591b4d9af96cb3167b50a5bf1e02dfea06c" + integrity sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw== + +"@rollup/rollup-openbsd-x64@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz#c51d48c07cd6c466560e5bed934aec688ce02614" + integrity sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw== + +"@rollup/rollup-openharmony-arm64@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz#f09921d0b2a0b60afbf3586d2a7a7f208ba6df17" + integrity sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ== + +"@rollup/rollup-win32-arm64-msvc@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz#08d491717135376e4a99529821c94ecd433d5b36" + integrity sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ== + +"@rollup/rollup-win32-ia32-msvc@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz#b0c12aac1104a8b8f26a5e0098e5facbb3e3964a" + integrity sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew== + +"@rollup/rollup-win32-x64-gnu@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz#b9cccef26f5e6fdc013bf3c0911a3c77428509d0" + integrity sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ== + +"@rollup/rollup-win32-x64-msvc@4.57.1": + version "4.57.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz#a03348e7b559c792b6277cc58874b89ef46e1e72" + integrity sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA== + +"@shikijs/core@2.5.0", "@shikijs/core@^2.1.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-2.5.0.tgz#e14d33961dfa3141393d4a76fc8923d0d1c4b62f" + integrity sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg== + dependencies: + "@shikijs/engine-javascript" "2.5.0" + "@shikijs/engine-oniguruma" "2.5.0" + "@shikijs/types" "2.5.0" + "@shikijs/vscode-textmate" "^10.0.2" + "@types/hast" "^3.0.4" + hast-util-to-html "^9.0.4" + +"@shikijs/engine-javascript@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz#e045c6ecfbda6c99137547b0a482e0b87f1053fc" + integrity sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w== + dependencies: + "@shikijs/types" "2.5.0" + "@shikijs/vscode-textmate" "^10.0.2" + oniguruma-to-es "^3.1.0" + +"@shikijs/engine-oniguruma@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz#230de5693cc1da6c9d59c7ad83593c2027274817" + integrity sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw== + dependencies: + "@shikijs/types" "2.5.0" + "@shikijs/vscode-textmate" "^10.0.2" + +"@shikijs/langs@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-2.5.0.tgz#97ab50c495922cc1ca06e192985b28dc73de5d50" + integrity sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w== + dependencies: + "@shikijs/types" "2.5.0" + +"@shikijs/themes@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-2.5.0.tgz#8c6aecf73f5455681c8bec15797cf678162896cb" + integrity sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw== + dependencies: + "@shikijs/types" "2.5.0" + +"@shikijs/transformers@^2.1.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@shikijs/transformers/-/transformers-2.5.0.tgz#190c84786ff06c417580ab79177338a947168c55" + integrity sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg== + dependencies: + "@shikijs/core" "2.5.0" + "@shikijs/types" "2.5.0" + +"@shikijs/types@2.5.0", "@shikijs/types@^2.1.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-2.5.0.tgz#e949c7384802703a48b9d6425dd41673c164df69" + integrity sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw== + dependencies: + "@shikijs/vscode-textmate" "^10.0.2" + "@types/hast" "^3.0.4" + +"@shikijs/vscode-textmate@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224" + integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg== + +"@types/estree@1.0.8", "@types/estree@^1.0.0", "@types/estree@^1.0.6": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/hast@^3.0.0", "@types/hast@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/linkify-it@^5": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" + integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== + +"@types/long@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" + integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== + +"@types/markdown-it@^14.1.2": + version "14.1.2" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" + integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== + dependencies: + "@types/linkify-it" "^5" + "@types/mdurl" "^2" + +"@types/mdast@^4.0.0": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" + integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== + dependencies: + "@types/unist" "*" + +"@types/mdurl@^2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" + integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== + +"@types/node@>=13.7.0": + version "25.0.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.10.tgz#4864459c3c9459376b8b75fd051315071c8213e7" + integrity sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg== + dependencies: + undici-types "~7.16.0" + +"@types/resolve@1.20.2": + version "1.20.2" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" + integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== + +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + +"@types/uuid@^3.4.6": + version "3.4.13" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.13.tgz#fe890e517fb840620be284ee213e81d702b1f76b" + integrity sha512-pAeZeUbLE4Z9Vi9wsWV2bYPTweEHeJJy0G4pEjOA/FSvy1Ad5U5Km8iDV6TKre1mjBiVNfAdVHKruP8bAh4Q5A== + +"@types/web-bluetooth@^0.0.21": + version "0.0.21" + resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz#525433c784aed9b457aaa0ee3d92aeb71f346b63" + integrity sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA== + +"@ungap/structured-clone@^1.0.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== + +"@vitejs/plugin-vue@^5.2.1": + version "5.2.4" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz#9e8a512eb174bfc2a333ba959bbf9de428d89ad8" + integrity sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA== + +"@vue/compiler-core@3.5.27": + version "3.5.27" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.27.tgz#ce4402428e26095586eb889c41f6e172eb3960bd" + integrity sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ== + dependencies: + "@babel/parser" "^7.28.5" + "@vue/shared" "3.5.27" + entities "^7.0.0" + estree-walker "^2.0.2" + source-map-js "^1.2.1" + +"@vue/compiler-dom@3.5.27": + version "3.5.27" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz#32b2bc87f0a652c253986796ace0ed6213093af8" + integrity sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w== + dependencies: + "@vue/compiler-core" "3.5.27" + "@vue/shared" "3.5.27" + +"@vue/compiler-sfc@3.5.27": + version "3.5.27" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz#84651b8816bf8e7d6e62fddd14db86efd6d6f1b6" + integrity sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ== + dependencies: + "@babel/parser" "^7.28.5" + "@vue/compiler-core" "3.5.27" + "@vue/compiler-dom" "3.5.27" + "@vue/compiler-ssr" "3.5.27" + "@vue/shared" "3.5.27" + estree-walker "^2.0.2" + magic-string "^0.30.21" + postcss "^8.5.6" + source-map-js "^1.2.1" + +"@vue/compiler-ssr@3.5.27": + version "3.5.27" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz#b480cad09dacf8f3d9c82b9843402f1a803baee7" + integrity sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw== + dependencies: + "@vue/compiler-dom" "3.5.27" + "@vue/shared" "3.5.27" + +"@vue/devtools-api@^7.7.0": + version "7.7.9" + resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-7.7.9.tgz#999dbea50da6b00cf59a1336f11fdc2b43d9e063" + integrity sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g== + dependencies: + "@vue/devtools-kit" "^7.7.9" + +"@vue/devtools-kit@^7.7.9": + version "7.7.9" + resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz#bc218a815616e8987df7ab3e10fc1fb3b8706c58" + integrity sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA== + dependencies: + "@vue/devtools-shared" "^7.7.9" + birpc "^2.3.0" + hookable "^5.5.3" + mitt "^3.0.1" + perfect-debounce "^1.0.0" + speakingurl "^14.0.1" + superjson "^2.2.2" + +"@vue/devtools-shared@^7.7.9": + version "7.7.9" + resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz#fa4c096b744927081a7dda5fcf05f34b1ae6ca14" + integrity sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA== + dependencies: + rfdc "^1.4.1" + +"@vue/reactivity@3.5.27": + version "3.5.27" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.27.tgz#d870557de1389a27b8abcb7cbfa30978dc69a000" + integrity sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ== + dependencies: + "@vue/shared" "3.5.27" + +"@vue/runtime-core@3.5.27": + version "3.5.27" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.27.tgz#bb43744ed070166c7d581b849ac22b71a9ccf127" + integrity sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A== + dependencies: + "@vue/reactivity" "3.5.27" + "@vue/shared" "3.5.27" + +"@vue/runtime-dom@3.5.27": + version "3.5.27" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz#392513252c7ca7e5277240fdc70b8093449127f5" + integrity sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg== + dependencies: + "@vue/reactivity" "3.5.27" + "@vue/runtime-core" "3.5.27" + "@vue/shared" "3.5.27" + csstype "^3.2.3" + +"@vue/server-renderer@3.5.27": + version "3.5.27" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.27.tgz#8137d0d7ec3b59d5992bb04c553775d209dddba7" + integrity sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA== + dependencies: + "@vue/compiler-ssr" "3.5.27" + "@vue/shared" "3.5.27" + +"@vue/shared@3.5.27", "@vue/shared@^3.5.13": + version "3.5.27" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.27.tgz#33a63143d8fb9ca1b3efbc7ecf9bd0ab05f7e06e" + integrity sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ== + +"@vueuse/core@12.8.2", "@vueuse/core@^12.4.0": + version "12.8.2" + resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-12.8.2.tgz#007c6dd29a7d1f6933e916e7a2f8ef3c3f968eaa" + integrity sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ== + dependencies: + "@types/web-bluetooth" "^0.0.21" + "@vueuse/metadata" "12.8.2" + "@vueuse/shared" "12.8.2" + vue "^3.5.13" + +"@vueuse/integrations@^12.4.0": + version "12.8.2" + resolved "https://registry.yarnpkg.com/@vueuse/integrations/-/integrations-12.8.2.tgz#d04f33d86fe985c9a27c98addcfde9f30f2db1df" + integrity sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g== + dependencies: + "@vueuse/core" "12.8.2" + "@vueuse/shared" "12.8.2" + vue "^3.5.13" + +"@vueuse/metadata@12.8.2": + version "12.8.2" + resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-12.8.2.tgz#6cb3a4e97cdcf528329eebc1bda73cd7f64318d3" + integrity sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A== + +"@vueuse/shared@12.8.2": + version "12.8.2" + resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-12.8.2.tgz#b9e4611d0603629c8e151f982459da394e22f930" + integrity sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w== + dependencies: + vue "^3.5.13" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.15.0: + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + 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" + +algoliasearch@^5.14.2: + version "5.47.0" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.47.0.tgz#ed95e008bf37806d320419f7f9ad329dc893c6fc" + integrity sha512-AGtz2U7zOV4DlsuYV84tLp2tBbA7RPtLA44jbVH4TTpDcc1dIWmULjHSsunlhscbzDydnjuFlNhflR3nV4VJaQ== + dependencies: + "@algolia/abtesting" "1.13.0" + "@algolia/client-abtesting" "5.47.0" + "@algolia/client-analytics" "5.47.0" + "@algolia/client-common" "5.47.0" + "@algolia/client-insights" "5.47.0" + "@algolia/client-personalization" "5.47.0" + "@algolia/client-query-suggestions" "5.47.0" + "@algolia/client-search" "5.47.0" + "@algolia/ingestion" "1.47.0" + "@algolia/monitoring" "1.47.0" + "@algolia/recommend" "5.47.0" + "@algolia/requester-browser-xhr" "5.47.0" + "@algolia/requester-fetch" "5.47.0" + "@algolia/requester-node-http" "5.47.0" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +birpc@^2.3.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/birpc/-/birpc-2.9.0.tgz#b59550897e4cd96a223e2a6c1475b572236ed145" + integrity sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw== + +brace-expansion@^1.1.7: + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + +commander@11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67" + integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== + +commander@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +copy-anything@^4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-4.0.5.tgz#16cabafd1ea4bb327a540b750f2b4df522825aea" + integrity sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA== + dependencies: + is-what "^5.2.0" + +core-js@3.33.1: + version "3.33.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.1.tgz#ef3766cfa382482d0a2c2bc5cb52c6d88805da52" + integrity sha512-qVSq3s+d4+GsqN0teRCJtM6tdEEXyWxjzbhVrCHmBS5ZTM0FS2MOS0D13dUXAWDUN6a+lHI/N1hF9Ytz6iLl9Q== + +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +csstype@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + +cucumber-messages@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/cucumber-messages/-/cucumber-messages-8.0.0.tgz#99766ffe026185798eb80fc8c720d60d8a6ac8cb" + integrity sha512-lUnWRMjwA9+KhDec/5xRZV3Du67ISumHnVLywWQXyvzmc4P+Eqx8CoeQrBQoau3Pw1hs4kJLTDyV85hFBF00SQ== + dependencies: + "@types/uuid" "^3.4.6" + protobufjs "^6.8.8" + uuid "^3.3.3" + +debug@^4.3.1, debug@^4.3.2: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +devlop@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + +emoji-regex-xs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz#e8af22e5d9dbd7f7f22d280af3d19d2aab5b0724" + integrity sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg== + +entities@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b" + integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA== + +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82" + integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" + integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== + +eslint@^9.39.1: + version "9.39.2" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.2.tgz#cb60e6d16ab234c0f8369a3fe7cc87967faf4b6c" + integrity sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw== + dependencies: + "@eslint-community/eslint-utils" "^4.8.0" + "@eslint-community/regexpp" "^4.12.1" + "@eslint/config-array" "^0.21.1" + "@eslint/config-helpers" "^0.4.2" + "@eslint/core" "^0.17.0" + "@eslint/eslintrc" "^3.3.1" + "@eslint/js" "9.39.2" + "@eslint/plugin-kit" "^0.4.1" + "@humanfs/node" "^0.16.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.4.2" + "@types/estree" "^1.0.6" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.6" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^8.4.0" + eslint-visitor-keys "^4.2.1" + espree "^10.4.0" + esquery "^1.5.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + 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" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + +espree@^10.0.1, espree@^10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837" + integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== + dependencies: + acorn "^8.15.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.1" + +esquery@^1.5.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d" + integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + dependencies: + flat-cache "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.4" + +flatted@^3.2.9: + version "3.3.3" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== + +flowbite-datepicker@^1.3.0, flowbite-datepicker@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/flowbite-datepicker/-/flowbite-datepicker-1.3.2.tgz#ad830d73f923344fb5614978f0d87e790cc69c4b" + integrity sha512-6Nfm0MCVX3mpaR7YSCjmEO2GO8CDt6CX8ZpQnGdeu03WUCWtEPQ/uy0PUiNtIJjJZWnX0Cm3H55MOhbD1g+E/g== + dependencies: + "@rollup/plugin-node-resolve" "^15.2.3" + flowbite "^2.0.0" + +flowbite@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/flowbite/-/flowbite-3.1.2.tgz#a3223462b608119e8388af6579d05642e553a5db" + integrity sha512-MkwSgbbybCYgMC+go6Da5idEKUFfMqc/AmSjm/2ZbdmvoKf5frLPq/eIhXc9P+rC8t9boZtUXzHDgt5whZ6A/Q== + dependencies: + "@popperjs/core" "^2.9.3" + flowbite-datepicker "^1.3.1" + mini-svg-data-uri "^1.4.3" + postcss "^8.5.1" + +flowbite@^2.0.0: + version "2.5.2" + resolved "https://registry.yarnpkg.com/flowbite/-/flowbite-2.5.2.tgz#4a14b87ad3f2abd8bcd7b0fb52a6b06fd7a74685" + integrity sha512-kwFD3n8/YW4EG8GlY3Od9IoKND97kitO+/ejISHSqpn3vw2i5K/+ZI8Jm2V+KC4fGdnfi0XZ+TzYqQb4Q1LshA== + dependencies: + "@popperjs/core" "^2.9.3" + flowbite-datepicker "^1.3.0" + mini-svg-data-uri "^1.4.3" + +focus-trap@^7.6.4: + version "7.8.0" + resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-7.8.0.tgz#b1d9463fa42b93ad7a5223d750493a6c09b672a8" + integrity sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA== + dependencies: + tabbable "^6.4.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gherkin-lint@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/gherkin-lint/-/gherkin-lint-4.2.4.tgz#5c1965d3c4ecf773d580917018823cf31f798116" + integrity sha512-iM+ECIHOF6Wh94YIF1hSHA6JH9rzcgozlMLHA/uCzGtQiMjb/uL093eh1nTpfoJ/38veL7Jfh4yY2inu7uUoFA== + 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" + +gherkin@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/gherkin/-/gherkin-9.0.0.tgz#dc1e52bb495f712f6de8f495eb7a2b655cbbabfd" + integrity sha512-6xoAepoxo5vhkBXjB4RCfVnSKHu5z9SqXIQVUyj+Jw8BQX8odATlee5otXgdN8llZvyvHokuvNiBeB3naEnnIQ== + dependencies: + commander "^4.0.1" + cucumber-messages "8.0.0" + source-map-support "^0.5.16" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + 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" + +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hast-util-to-html@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz#ccc673a55bb8e85775b08ac28380f72d47167005" + integrity sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw== + 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" + +hast-util-whitespace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" + integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== + dependencies: + "@types/hast" "^3.0.0" + +hookable@^5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d" + integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ== + +html-void-elements@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" + integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +import-fresh@^3.2.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-core-module@^2.16.1: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.0, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + +is-what@^5.2.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-5.5.0.tgz#a3031815757cfe1f03fed990bf6355a2d3f628c4" + integrity sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-yaml@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== + dependencies: + argparse "^2.0.1" + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash@4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + +magic-string@^0.30.21: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + +mark.js@8.11.1: + version "8.11.1" + resolved "https://registry.yarnpkg.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5" + integrity sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ== + +mdast-util-to-hast@^13.0.0: + version "13.2.1" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz#d7ff84ca499a57e2c060ae67548ad950e689a053" + integrity sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA== + 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" + +micromark-util-character@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6" + integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8" + integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7" + integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-symbol@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8" + integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== + +micromark-util-types@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e" + integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== + +mini-svg-data-uri@^1.4.3: + version "1.4.4" + resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939" + integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg== + +minimatch@^3.0.4, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minisearch@^7.1.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/minisearch/-/minisearch-7.2.0.tgz#3dc30e41e9464b3836553b6d969b656614f8f359" + integrity sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg== + +mitt@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" + integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.11: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +oniguruma-to-es@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz#480e4bac4d3bc9439ac0d2124f0725e7a0d76d17" + integrity sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ== + dependencies: + emoji-regex-xs "^1.0.0" + regex "^6.0.1" + regex-recursion "^6.0.2" + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + 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" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +perfect-debounce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a" + integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + +postcss@^8.4.43, postcss@^8.5.1, postcss@^8.5.6: + version "8.5.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +preact@^10.0.0: + version "10.28.2" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.28.2.tgz#4b668383afa4b4a2546bbe4bd1747e02e2360138" + integrity sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +property-information@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-7.1.0.tgz#b622e8646e02b580205415586b40804d3e8bfd5d" + integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== + +protobufjs@^6.8.8: + version "6.11.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.4.tgz#29a412c38bf70d89e537b6d02d904a6f448173aa" + integrity sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw== + 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" + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +regex-recursion@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/regex-recursion/-/regex-recursion-6.0.2.tgz#a0b1977a74c87f073377b938dbedfab2ea582b33" + integrity sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg== + dependencies: + regex-utilities "^2.3.0" + +regex-utilities@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/regex-utilities/-/regex-utilities-2.3.0.tgz#87163512a15dce2908cf079c8960d5158ff43280" + integrity sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng== + +regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/regex/-/regex-6.1.0.tgz#d7ce98f8ee32da7497c13f6601fca2bc4a6a7803" + integrity sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg== + dependencies: + regex-utilities "^2.3.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.22.1: + version "1.22.11" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== + dependencies: + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rfdc@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + +rollup@^4.20.0, rollup@^4.56.0: + version "4.57.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.57.1.tgz#947f70baca32db2b9c594267fe9150aa316e5a88" + integrity sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A== + dependencies: + "@types/estree" "1.0.8" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.57.1" + "@rollup/rollup-android-arm64" "4.57.1" + "@rollup/rollup-darwin-arm64" "4.57.1" + "@rollup/rollup-darwin-x64" "4.57.1" + "@rollup/rollup-freebsd-arm64" "4.57.1" + "@rollup/rollup-freebsd-x64" "4.57.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.57.1" + "@rollup/rollup-linux-arm-musleabihf" "4.57.1" + "@rollup/rollup-linux-arm64-gnu" "4.57.1" + "@rollup/rollup-linux-arm64-musl" "4.57.1" + "@rollup/rollup-linux-loong64-gnu" "4.57.1" + "@rollup/rollup-linux-loong64-musl" "4.57.1" + "@rollup/rollup-linux-ppc64-gnu" "4.57.1" + "@rollup/rollup-linux-ppc64-musl" "4.57.1" + "@rollup/rollup-linux-riscv64-gnu" "4.57.1" + "@rollup/rollup-linux-riscv64-musl" "4.57.1" + "@rollup/rollup-linux-s390x-gnu" "4.57.1" + "@rollup/rollup-linux-x64-gnu" "4.57.1" + "@rollup/rollup-linux-x64-musl" "4.57.1" + "@rollup/rollup-openbsd-x64" "4.57.1" + "@rollup/rollup-openharmony-arm64" "4.57.1" + "@rollup/rollup-win32-arm64-msvc" "4.57.1" + "@rollup/rollup-win32-ia32-msvc" "4.57.1" + "@rollup/rollup-win32-x64-gnu" "4.57.1" + "@rollup/rollup-win32-x64-msvc" "4.57.1" + fsevents "~2.3.2" + +sax@^1.2.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.4.tgz#f29c2bba80ce5b86f4343b4c2be9f2b96627cf8b" + integrity sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shiki@^2.1.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-2.5.0.tgz#09d01ebf3b0b06580431ce3ddc023320442cf223" + integrity sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ== + dependencies: + "@shikijs/core" "2.5.0" + "@shikijs/engine-javascript" "2.5.0" + "@shikijs/engine-oniguruma" "2.5.0" + "@shikijs/langs" "2.5.0" + "@shikijs/themes" "2.5.0" + "@shikijs/types" "2.5.0" + "@shikijs/vscode-textmate" "^10.0.2" + "@types/hast" "^3.0.4" + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map-support@^0.5.16: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + +speakingurl@^14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53" + integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ== + +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + +strip-json-comments@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" + integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +superjson@^2.2.2: + version "2.2.6" + resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.6.tgz#a223a3a988172a5f9656e2063fe5f733af40d099" + integrity sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA== + dependencies: + copy-anything "^4" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tabbable@^6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.4.0.tgz#36eb7a06d80b3924a22095daf45740dea3bf5581" + integrity sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg== + +tailwindcss@^4.1.18: + version "4.1.18" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.1.18.tgz#f488ba47853abdb5354daf9679d3e7791fc4f4e3" + integrity sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw== + +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +undici-types@~7.16.0: + version "7.16.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== + +unist-util-is@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.1.tgz#d0a3f86f2dd0db7acd7d8c2478080b5c67f9c6a9" + integrity sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" + integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-visit-parents@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz#777df7fb98652ce16b4b7cd999d0a1a40efa3a02" + integrity sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.1.0.tgz#9a2a28b0aa76a15e0da70a08a5863a2f060e2468" + integrity sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +uuid@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +vfile-message@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4" + integrity sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + +vfile@^6.0.0: + version "6.0.3" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" + integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== + dependencies: + "@types/unist" "^3.0.0" + vfile-message "^4.0.0" + +vite@^5.4.14: + version "5.4.21" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.21.tgz#84a4f7c5d860b071676d39ba513c0d598fdc7027" + integrity sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.43" + rollup "^4.20.0" + optionalDependencies: + fsevents "~2.3.3" + +vitepress@^1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/vitepress/-/vitepress-1.6.4.tgz#1b6c68fede541a3f401a66263dce0c985e2d8d92" + integrity sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg== + dependencies: + "@docsearch/css" "3.8.2" + "@docsearch/js" "3.8.2" + "@iconify-json/simple-icons" "^1.2.21" + "@shikijs/core" "^2.1.0" + "@shikijs/transformers" "^2.1.0" + "@shikijs/types" "^2.1.0" + "@types/markdown-it" "^14.1.2" + "@vitejs/plugin-vue" "^5.2.1" + "@vue/devtools-api" "^7.7.0" + "@vue/shared" "^3.5.13" + "@vueuse/core" "^12.4.0" + "@vueuse/integrations" "^12.4.0" + focus-trap "^7.6.4" + mark.js "8.11.1" + minisearch "^7.1.1" + shiki "^2.1.0" + vite "^5.4.14" + vue "^3.5.13" + +vue@^3.5.13: + version "3.5.27" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.27.tgz#e55fd941b614459ab2228489bc19d1692e05876c" + integrity sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw== + dependencies: + "@vue/compiler-dom" "3.5.27" + "@vue/compiler-sfc" "3.5.27" + "@vue/runtime-dom" "3.5.27" + "@vue/server-renderer" "3.5.27" + "@vue/shared" "3.5.27" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +xml-js@^1.6.11: + version "1.6.11" + resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" + integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== + dependencies: + sax "^1.2.4" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zwitch@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==