diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..231ace72ff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,25 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "" +labels: "" +assignees: "" +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Reproduction steps** +Steps to reproduce the issue seen. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional context** + +- `rails` version: +- `rails_admin` version: +- `rails_admin` npm package version: +- full stack trace (if there's an exception) + +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..6d71f7732e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,16 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "" +labels: enhancement +assignees: "" +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe proposed solution(s)** +A clear and concise description of what you want to happen. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/code-ql.yml b/.github/workflows/code-ql.yml new file mode 100644 index 0000000000..f723752131 --- /dev/null +++ b/.github/workflows/code-ql.yml @@ -0,0 +1,37 @@ +name: "CodeQL" + +on: + push: + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + schedule: + - cron: "12 00 * * 5" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["ruby"] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.gitignore b/.gitignore index 88aa68691b..cce19e8d3e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,15 +4,18 @@ *.swp .bundle .idea/ +.vscode/ .rvmrc +.sass-cache .yardoc /.emacs.desktop /gemfiles/*.lock +/node_modules/* /rails_admin.gems /spec/generators/tmp /spec/lib/tmp +/yarn.lock Gemfile.lock -Gemfile31.lock coverage/* db/*.sqlite3 db/*.sqlite3-journal @@ -24,9 +27,6 @@ spec/dummy_app/db/*.sqlite3-journal spec/dummy_app/db/schema.rb spec/dummy_app/log/*.log spec/dummy_app/public/uploads -spec/dummy_app/Gemfile.lock +spec/dummy_app/Gemfile*.lock tmp/**/* -/.emacs.desktop -.idea/*.xml -.sass-cache nbproject diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..aa6741b99c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +coverage +lib/generators/rails_admin/templates +spec/dummy_app/app/assets/builds +spec/dummy_app/public +spec/dummy_app/tmp +spec/support/jquery.simulate.drag-sortable.js +vendor diff --git a/.rspec b/.rspec index 97f3a5d316..af054e7f5c 100644 --- a/.rspec +++ b/.rspec @@ -1,3 +1,4 @@ --color --order=random --profile +--exclude-pattern 'dummy_app/node_modules/rails_admin/**/*_spec.rb' diff --git a/.rubocop.yml b/.rubocop.yml index 407314f8ec..5c76364890 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,12 +1,23 @@ inherit_from: .rubocop_todo.yml +require: + - rubocop-performance + AllCops: Exclude: - - 'gemfiles/*' - - 'spec/dummy_app/bin/**/*' - - 'spec/dummy_app/db/schema.rb' - - 'spec/dummy_app/tmp/**/*' - - 'vendor/bundle/**/*' + - "gemfiles/*" + - "node_modules/**/*" + - "spec/dummy_app/bin/**/*" + - "spec/dummy_app/db/schema.rb" + - "spec/dummy_app/node_modules/**/*" + - "spec/dummy_app/tmp/**/*" + - "vendor/bundle/**/*" + NewCops: disable + SuggestExtensions: false + TargetRubyVersion: 2.6 + +Gemspec/DeprecatedAttributeAssignment: + Enabled: true Layout/AccessModifierIndentation: EnforcedStyle: outdent @@ -14,9 +25,82 @@ Layout/AccessModifierIndentation: Layout/DotPosition: EnforcedStyle: trailing +Layout/LineEndStringConcatenationIndentation: + Enabled: true + +Layout/LineLength: + AllowURI: true + Enabled: false + +Layout/MultilineAssignmentLayout: + Enabled: true + SupportedTypes: [case, if] + +Layout/SpaceBeforeBrackets: + Enabled: true + Layout/SpaceInsideHashLiteralBraces: EnforcedStyle: no_space +Lint/AmbiguousAssignment: + Enabled: true + +Lint/AmbiguousRange: + Enabled: true + +Lint/DeprecatedConstants: + Enabled: true + +Lint/DuplicateBranch: + Enabled: true + IgnoreLiteralBranches: true + +Lint/DuplicateRegexpCharacterClassElement: + Enabled: true + +Lint/EmptyBlock: + Enabled: true + Exclude: + - "spec/**/*" + +Lint/EmptyClass: + Enabled: true + Exclude: + - "spec/**/*" + +Lint/EmptyInPattern: + Enabled: true + +Lint/LambdaWithoutLiteralBlock: + Enabled: true + +Lint/NoReturnInBeginEndBlocks: + Enabled: true + +Lint/NumberedParameterAssignment: + Enabled: true + +Lint/OrAssignmentToConstant: + Enabled: true + +Lint/RedundantDirGlobSort: + Enabled: true + +Lint/SymbolConversion: + Enabled: true + +Lint/ToEnumArguments: + Enabled: true + +Lint/TripleQuotes: + Enabled: true + +Lint/UnexpectedBlockArity: + Enabled: true + +Lint/UnmodifiedReduceAccumulator: + Enabled: true + Metrics/AbcSize: Max: 69.98 # TODO: Lower to 15 @@ -25,69 +109,116 @@ Metrics/BlockNesting: Metrics/ClassLength: CountComments: false - Max: 120 # TODO: Lower to 100 + Max: 201 # TODO: Lower to 100 Metrics/CyclomaticComplexity: - Max: 12 # TODO: Lower to 6 - -Metrics/LineLength: - AllowURI: true - Enabled: false + Max: 15 # TODO: Lower to 6 Metrics/MethodLength: CountComments: false Max: 29 # TODO: Lower to 15 Metrics/ModuleLength: - Max: 200 # TODO: Lower to 100 + Max: 219 # TODO: Lower to 100 Metrics/ParameterLists: Max: 8 # TODO: Lower to 4 CountKeywordArgs: true Metrics/PerceivedComplexity: - Max: 14 # TODO: Lower to 7 + Max: 17 # TODO: Lower to 7 Naming/FileName: Exclude: - - 'lib/rails_admin/bootstrap-sass.rb' + - "lib/rails_admin/bootstrap-sass.rb" + +Naming/InclusiveLanguage: + Enabled: true Style/Alias: Enabled: false +Style/ArgumentsForwarding: + Enabled: true + +Style/CollectionCompact: + Enabled: true + Style/CollectionMethods: PreferredMethods: - map: 'collect' - reduce: 'inject' - find: 'detect' - find_all: 'select' + map: "collect" + reduce: "inject" + find: "detect" + find_all: "select" Style/Documentation: Enabled: false +Style/DocumentDynamicEvalDefinition: + Enabled: false + Style/DoubleNegation: Enabled: false Style/EachWithObject: Enabled: false -Style/Encoding: - Enabled: false +Style/EndlessMethod: + Enabled: true -Style/FrozenStringLiteralComment: - Enabled: false +Style/HashConversion: + Enabled: true + +Style/HashExcept: + Enabled: true + +Style/IfWithBooleanLiteralBranches: + Enabled: true + +Style/InPatternThen: + Enabled: true Style/Lambda: Enabled: false +Style/MultilineInPatternThen: + Enabled: true + +Style/NegatedIfElseCondition: + Enabled: true + +Style/NumericPredicate: + Enabled: false + +Style/NilLambda: + Enabled: true + +Style/QuotedSymbols: + Enabled: true + Style/RaiseArgs: EnforcedStyle: compact +Style/RedundantArgument: + Enabled: true + +Style/RedundantParentheses: + Enabled: false + +Style/RedundantSelfAssignmentBranch: + Enabled: true + +Style/StringChars: + Enabled: true + +Style/SwapValues: + Enabled: true + Style/TrailingCommaInArguments: - EnforcedStyleForMultiline: 'comma' + EnforcedStyleForMultiline: "comma" Style/TrailingCommaInArrayLiteral: - EnforcedStyleForMultiline: 'comma' + EnforcedStyleForMultiline: "comma" Style/TrailingCommaInHashLiteral: - EnforcedStyleForMultiline: 'comma' + EnforcedStyleForMultiline: "comma" diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d2f1fa8a3c..447ccf2ad2 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,400 +1,129 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2019-12-10 17:46:01 +0900 using RuboCop version 0.68.1. +# on 2021-11-20 13:57:54 UTC using RuboCop version 1.23.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 35 -# Cop supports --auto-correct. -# Configuration parameters: TreatCommentsAsGroupSeparators, Include. -# Include: **/*.gemfile, **/Gemfile, **/gems.rb -Bundler/OrderedGems: - Exclude: - - 'Gemfile' - - 'gemfiles/rails_5.0.gemfile' - - 'gemfiles/rails_5.1.gemfile' - - 'gemfiles/rails_5.2.gemfile' - - 'gemfiles/rails_6.0.gemfile' - - 'spec/dummy_app/Gemfile' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: TreatCommentsAsGroupSeparators, Include. -# Include: **/*.gemspec -Gemspec/OrderedDependencies: - Exclude: - - 'rails_admin.gemspec' - -# Offense count: 2 -# Cop supports --auto-correct. -Layout/ClosingHeredocIndentation: - Exclude: - - 'lib/rails_admin/adapters/mongoid/abstract_object.rb' - -# Offense count: 53 -# Cop supports --auto-correct. -Layout/EmptyLineAfterGuardClause: +# Offense count: 51 +# Configuration parameters: AllowedMethods. +# AllowedMethods: enums +Lint/ConstantDefinitionInBlock: Enabled: false -# Offense count: 5 -# Cop supports --auto-correct. -Layout/EmptyLineAfterMagicComment: - Exclude: - - 'lib/rails_admin/support/csv_converter.rb' - - 'rails_admin.gemspec' - - 'spec/controllers/rails_admin/main_controller_spec.rb' - - 'spec/dummy_app/app/active_record/carrierwave_uploader.rb' - - 'spec/dummy_app/app/mongoid/carrierwave_uploader.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent -Layout/IndentHeredoc: - Exclude: - - 'lib/rails_admin/engine.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: Width, IgnoredPatterns. -Layout/IndentationWidth: - Exclude: - - 'lib/rails_admin/adapters/mongoid.rb' - - 'spec/spec_helper.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Layout/LeadingBlankLines: - Exclude: - - 'lib/rails_admin/extension.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. -# SupportedStyles: space, no_space -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceBeforeBlockBraces: - Exclude: - - 'spec/rails_admin/install_generator_spec.rb' - -# Offense count: 1 -Lint/AmbiguousBlockAssociation: - Exclude: - - 'spec/rails_admin/install_generator_spec.rb' - -# Offense count: 18 -# Configuration parameters: AllowSafeAssignment. -Lint/AssignmentInCondition: - Exclude: - - 'app/controllers/rails_admin/main_controller.rb' - - 'app/helpers/rails_admin/application_helper.rb' - - 'lib/rails_admin/abstract_model.rb' - - 'lib/rails_admin/adapters/active_record.rb' - - 'lib/rails_admin/config/actions/export.rb' - - 'lib/rails_admin/config/actions/index.rb' - - 'lib/rails_admin/config/actions/new.rb' - - 'lib/rails_admin/config/fields/factories/active_storage.rb' - - 'lib/rails_admin/config/fields/factories/association.rb' - - 'lib/rails_admin/config/fields/factories/carrierwave.rb' - - 'lib/rails_admin/config/fields/factories/devise.rb' - - 'lib/rails_admin/config/fields/factories/dragonfly.rb' - - 'lib/rails_admin/config/fields/factories/paperclip.rb' - - 'lib/rails_admin/config/fields/types/datetime.rb' - - 'spec/dummy_app/db/seeds.rb' - -# Offense count: 1 -Lint/DuplicateMethods: - Exclude: - - 'lib/rails_admin/config.rb' - # Offense count: 1 Lint/ReturnInVoidContext: Exclude: - - 'lib/rails_admin/support/csv_converter.rb' + - "lib/rails_admin/support/csv_converter.rb" -# Offense count: 1 -# Cop supports --auto-correct. -Lint/ScriptPermission: - Exclude: - - 'spec/dummy_app/Rakefile' - -# Offense count: 2 -# Cop supports --auto-correct. -Lint/UnneededSplatExpansion: - Exclude: - - 'spec/rails_admin/adapters/active_record/association_spec.rb' - -# Offense count: 198 -# Configuration parameters: CountComments, ExcludedMethods. -# ExcludedMethods: refine +# Offense count: 226 +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. +# IgnoredMethods: refine Metrics/BlockLength: - Max: 1081 - Exclude: - - 'spec/integration/actions/index_spec.rb' + Max: 1135 -# Offense count: 7 -Naming/AccessorMethodName: - Exclude: - - 'app/controllers/rails_admin/application_controller.rb' - - 'app/controllers/rails_admin/main_controller.rb' - - 'lib/rails_admin/abstract_model.rb' - - 'lib/rails_admin/adapters/active_record/abstract_object.rb' +# Offense count: 1 +# Configuration parameters: Max, CountKeywordArgs. +Metrics/ParameterLists: + MaxOptionalParameters: 4 # Offense count: 6 -# Configuration parameters: Blacklist. -# Blacklist: (?-mix:(^|\s)(EO[A-Z]{1}|END)(\s|$)) -Naming/HeredocDelimiterNaming: +Naming/AccessorMethodName: Exclude: - - 'app/controllers/rails_admin/main_controller.rb' - - 'lib/rails_admin/adapters/mongoid.rb' - - 'lib/rails_admin/config/fields/base.rb' - - 'lib/rails_admin/engine.rb' - - 'lib/rails_admin/extensions/paper_trail/auditing_adapter.rb' - - 'spec/factories.rb' + - "app/controllers/rails_admin/application_controller.rb" + - "app/controllers/rails_admin/main_controller.rb" + - "lib/rails_admin/abstract_model.rb" # Offense count: 2 # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: Exclude: - - 'app/controllers/rails_admin/application_controller.rb' - - 'lib/rails_admin/config/has_description.rb' + - "app/controllers/rails_admin/application_controller.rb" + - "lib/rails_admin/config/has_description.rb" -# Offense count: 3 +# Offense count: 2 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. -# AllowedNames: io, id, to, by, on, in, at, ip, db -Naming/UncommunicativeMethodParamName: +# AllowedNames: at, by, db, id, in, io, ip, of, on, os, pp, to +Naming/MethodParameterName: Exclude: - - 'lib/rails_admin/abstract_model.rb' - - 'lib/rails_admin/config/model.rb' - - 'spec/rails_admin/adapters/mongoid/property_spec.rb' + - "lib/rails_admin/abstract_model.rb" + - "spec/rails_admin/adapters/mongoid/property_spec.rb" # Offense count: 1 -# Configuration parameters: EnforcedStyle. +# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers. # SupportedStyles: snake_case, normalcase, non_integer +# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339 Naming/VariableNumber: Exclude: - - 'spec/helpers/rails_admin/application_helper_spec.rb' + - "spec/helpers/rails_admin/application_helper_spec.rb" # Offense count: 11 Style/ClassVars: Exclude: - - 'lib/rails_admin/abstract_model.rb' - - 'lib/rails_admin/config.rb' - - 'lib/rails_admin/config/actions.rb' - - 'lib/rails_admin/config/fields.rb' - - 'lib/rails_admin/config/fields/types.rb' - -# Offense count: 1 -Style/CommentedKeyword: - Exclude: - - 'app/helpers/rails_admin/application_helper.rb' + - "lib/rails_admin/abstract_model.rb" + - "lib/rails_admin/config.rb" + - "lib/rails_admin/config/actions.rb" + - "lib/rails_admin/config/fields.rb" + - "lib/rails_admin/config/fields/types.rb" # Offense count: 2 -Style/EvalWithLocation: - Exclude: - - 'lib/rails_admin/config/actions.rb' +# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, IgnoredMethods. +# SupportedStyles: annotated, template, unannotated +Style/FormatStringToken: + EnforcedStyle: template -# Offense count: 15 -# Cop supports --auto-correct. -Style/ExpandPathArguments: - Exclude: - - 'lib/generators/rails_admin/install_generator.rb' - - 'rails_admin.gemspec' - - 'spec/dummy_app/Rakefile' - - 'spec/dummy_app/config/application.rb' - - 'spec/dummy_app/config/boot.rb' - - 'spec/dummy_app/config/environment.rb' - - 'spec/integration/fields/multiple_carrierwave_spec.rb' - - 'spec/orm/active_record.rb' - - 'spec/rails_admin/active_record_extension_spec.rb' - - 'spec/rails_admin/install_generator_spec.rb' - - 'spec/spec_helper.rb' - -# Offense count: 9 +# Offense count: 8 # Configuration parameters: MinBodyLength. Style/GuardClause: Exclude: - - 'lib/rails_admin.rb' - - 'lib/rails_admin/adapters/active_record.rb' - - 'lib/rails_admin/bootstrap-sass.rb' - - 'lib/rails_admin/config.rb' - - 'lib/rails_admin/config/actions.rb' - - 'lib/rails_admin/config/fields/types/polymorphic_association.rb' - - 'lib/rails_admin/config/inspectable.rb' - - 'lib/rails_admin/config/sections/list.rb' - - 'lib/rails_admin/extension.rb' + - "app/helpers/rails_admin/main_helper.rb" + - "lib/rails_admin.rb" + - "lib/rails_admin/bootstrap-sass.rb" + - "lib/rails_admin/config.rb" + - "lib/rails_admin/config/actions.rb" + - "lib/rails_admin/config/fields/types/polymorphic_association.rb" + - "lib/rails_admin/config/sections/list.rb" # Offense count: 2 -Style/IdenticalConditionalBranches: +Style/MissingRespondToMissing: Exclude: - - 'spec/integration/authorization/cancancan_spec.rb' + - "lib/rails_admin/config/model.rb" + - "lib/rails_admin/config/proxyable/proxy.rb" # Offense count: 17 -# Cop supports --auto-correct. -Style/IfUnlessModifier: - Exclude: - - 'config/initializers/haml.rb' - - 'lib/rails_admin/adapters/active_record.rb' - - 'lib/rails_admin/adapters/mongoid.rb' - - 'lib/rails_admin/bootstrap-sass.rb' - - 'lib/rails_admin/config/actions/index.rb' - - 'lib/rails_admin/config/fields/types/active_storage.rb' - - 'lib/rails_admin/config/fields/types/multiple_active_storage.rb' - - 'lib/rails_admin/config/inspectable.rb' - - 'lib/rails_admin/extension.rb' - - 'lib/rails_admin/extensions/history/history.rb' - - 'lib/rails_admin/extensions/pundit/authorization_adapter.rb' - - 'lib/rails_admin/support/csv_converter.rb' +# Configuration parameters: AllowedMethods. +# AllowedMethods: respond_to_missing? +Style/OptionalBooleanParameter: + Exclude: + - "app/helpers/rails_admin/application_helper.rb" + - "lib/rails_admin/config/fields/types/active_storage.rb" + - "lib/rails_admin/config/fields/types/carrierwave.rb" + - "lib/rails_admin/config/fields/types/dragonfly.rb" + - "lib/rails_admin/config/fields/types/multiple_active_storage.rb" + - "lib/rails_admin/config/fields/types/multiple_carrierwave.rb" + - "lib/rails_admin/config/fields/types/multiple_file_upload.rb" + - "lib/rails_admin/config/fields/types/paperclip.rb" + - "lib/rails_admin/config/has_fields.rb" + - "spec/integration/fields/file_upload_spec.rb" + - "spec/integration/fields/multiple_file_upload_spec.rb" + - "spec/orm/active_record.rb" + - "spec/orm/mongoid.rb" + - "spec/rails_admin/config/fields/types/multiple_file_upload_spec.rb" # Offense count: 3 # Cop supports --auto-correct. -# Configuration parameters: InverseMethods, InverseBlocks. -Style/InverseMethods: - Exclude: - - 'lib/rails_admin/config/fields.rb' - - 'lib/rails_admin/config/fields/types/enum.rb' - - 'lib/rails_admin/config/fields/types/polymorphic_association.rb' - -# Offense count: 4 -Style/MethodMissingSuper: +# Configuration parameters: Mode. +Style/StringConcatenation: Exclude: - - 'lib/rails_admin/adapters/active_record/abstract_object.rb' - - 'lib/rails_admin/config/lazy_model.rb' - - 'lib/rails_admin/config/model.rb' - - 'lib/rails_admin/config/proxyable/proxy.rb' - -# Offense count: 4 -Style/MissingRespondToMissing: - Exclude: - - 'lib/rails_admin/adapters/active_record/abstract_object.rb' - - 'lib/rails_admin/config/lazy_model.rb' - - 'lib/rails_admin/config/model.rb' - - 'lib/rails_admin/config/proxyable/proxy.rb' + - "app/helpers/rails_admin/application_helper.rb" + - "lib/rails_admin/extensions/paper_trail/auditing_adapter.rb" # Offense count: 2 -Style/MixinUsage: - Exclude: - - 'spec/controllers/rails_admin/main_controller_spec.rb' - -# Offense count: 14 -# Cop supports --auto-correct. -Style/MultilineIfModifier: - Exclude: - - 'app/helpers/rails_admin/main_helper.rb' - - 'lib/rails_admin/extensions/pundit/authorization_adapter.rb' - - 'spec/controllers/rails_admin/main_controller_spec.rb' - - 'spec/integration/fields/action_text_spec.rb' - - 'spec/rails_admin/config/fields/types/action_text_spec.rb' - - 'spec/rails_admin/config/fields/types/active_storage_spec.rb' - - 'spec/rails_admin/config/fields/types/file_upload_spec.rb' - - 'spec/rails_admin/config/fields/types/multiple_active_storage_spec.rb' - - 'spec/rails_admin/config/fields/types/multiple_file_upload_spec.rb' - - 'spec/rails_admin/config/fields/types/shrine_spec.rb' - - 'spec/rails_admin/config_spec.rb' - -# Offense count: 1 -Style/MultipleComparison: - Exclude: - - 'lib/rails_admin/abstract_model.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods. -# SupportedStyles: predicate, comparison -Style/NumericPredicate: - Exclude: - - 'spec/**/*' - - 'lib/rails_admin/config.rb' - - 'lib/rails_admin/config/fields/types/string.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Style/OrAssignment: - Exclude: - - 'lib/rails_admin/config/sections.rb' - -# Offense count: 70 -# Cop supports --auto-correct. -# Configuration parameters: PreferredDelimiters. -Style/PercentLiteralDelimiters: - Enabled: false - -# Offense count: 1 -# Cop supports --auto-correct. -Style/RedundantSelf: - Exclude: - - 'lib/rails_admin/config/fields/base.rb' - -# Offense count: 7 -# Cop supports --auto-correct. -Style/RescueModifier: - Exclude: - - 'app/helpers/rails_admin/application_helper.rb' - - 'lib/rails_admin/abstract_model.rb' - - 'lib/rails_admin/adapters/mongoid/association.rb' - - 'lib/rails_admin/config/actions/show_in_app.rb' - - 'lib/rails_admin/extensions/paper_trail/auditing_adapter.rb' - -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: implicit, explicit -Style/RescueStandardError: - Exclude: - - 'lib/rails_admin/adapters/mongoid.rb' - - 'lib/rails_admin/adapters/mongoid/bson.rb' - - 'lib/rails_admin/support/i18n.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Style/StderrPuts: - Exclude: - - 'Rakefile' - -# Offense count: 528 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. # SupportedStyles: single_quotes, double_quotes Style/StringLiterals: - Enabled: false - -# Offense count: 93 -# Cop supports --auto-correct. -# Configuration parameters: . -# SupportedStyles: percent, brackets -Style/SymbolArray: - EnforcedStyle: percent - MinSize: 12 - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, AllowSafeAssignment. -# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex -Style/TernaryParentheses: - Exclude: - - 'spec/spec_helper.rb' - -# Offense count: 10 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, MinSize, WordRegex. -# SupportedStyles: percent, brackets -Style/WordArray: - Exclude: - - 'lib/rails_admin/adapters/active_record.rb' - - 'lib/rails_admin/support/datetime.rb' - - 'lib/rails_admin/support/i18n.rb' - - 'spec/rails_admin/abstract_model_spec.rb' - - 'spec/rails_admin/adapters/active_record_spec.rb' - -# Offense count: 4 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: forbid_for_all_comparison_operators, forbid_for_equality_operators_only, require_for_all_comparison_operators, require_for_equality_operators_only -Style/YodaCondition: Exclude: - - 'lib/rails_admin/config/fields/base.rb' - - 'lib/rails_admin/config/hideable.rb' - - 'lib/tasks/rails_admin.rake' + - "app/helpers/rails_admin/application_helper.rb" diff --git a/Appraisals b/Appraisals index 93d3a6eae5..fc378a6d4c 100644 --- a/Appraisals +++ b/Appraisals @@ -1,153 +1,103 @@ -appraise "rails-5.0" do - gem 'rails', '~> 5.0.0' - gem 'sassc-rails', '~> 2.1' - gem 'devise', '~> 4.0' +# frozen_string_literal: true + +appraise 'rails-6.0' do + gem 'rails', '~> 6.0.0' + gem 'psych', '~> 3.3' + gem 'turbo-rails', ['!= 2.0.8', '!= 2.0.9'] group :test do - gem 'cancancan', '~> 2.0' - gem 'paperclip', ['>= 3.4', '!= 4.3.0'] - gem 'shrine', '~> 2.13.0' - gem 'shrine-memory' + gem 'cancancan', ['~> 3.0', '< 3.6'] + gem 'pundit', '~> 2.1.0' end group :active_record do - gem 'paper_trail', '>= 5.0' - - platforms :ruby, :mswin, :mingw, :x64_mingw do - gem 'sqlite3', '~> 1.3.0' - end - platforms :jruby do - gem 'activerecord-jdbcmysql-adapter', '~> 50.0' - gem 'activerecord-jdbcpostgresql-adapter', '~> 50.0' - gem 'activerecord-jdbcsqlite3-adapter', '~> 50.0' + gem 'activerecord-jdbcmysql-adapter', '~> 60.0' + gem 'activerecord-jdbcpostgresql-adapter', '~> 60.0' + gem 'activerecord-jdbcsqlite3-adapter', '~> 60.0' end end group :mongoid do - gem 'mongoid', '~> 6.1' + gem 'cancancan-mongoid' + gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' + gem 'database_cleaner-mongoid', '>= 2.0', require: false gem 'kaminari-mongoid' + gem 'mongoid', '~> 7.0' gem 'mongoid-paperclip', '>= 0.0.8', require: 'mongoid_paperclip' - gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' - gem 'cancancan-mongoid' - gem 'shrine-mongoid', '~> 0.2.4' + gem 'shrine-mongoid', '~> 1.0' end end -appraise "rails-5.1" do - gem 'rails', '~> 5.1.0' - gem 'sassc-rails', '~> 2.1' - gem 'devise', '~> 4.0' - - group :test do - gem 'cancancan', '~> 2.0' - gem 'paperclip', ['>= 3.4', '!= 4.3.0'] - gem 'shrine', '~> 3.0' - end +appraise 'rails-6.1' do + gem 'rails', '~> 6.1.0' group :active_record do - gem 'pg', '~> 0.14', platforms: :ruby - gem 'paper_trail', '>= 5.0' - platforms :jruby do - gem 'activerecord-jdbcmysql-adapter', '~> 51.0' - gem 'activerecord-jdbcpostgresql-adapter', '~> 51.0' - gem 'activerecord-jdbcsqlite3-adapter', '~> 51.0' + gem 'activerecord-jdbcmysql-adapter', '~> 61.0' + gem 'activerecord-jdbcpostgresql-adapter', '~> 61.0' + gem 'activerecord-jdbcsqlite3-adapter', '~> 61.0' end end group :mongoid do - gem 'mongoid', '~> 7.0' + gem 'cancancan-mongoid' + gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' + gem 'database_cleaner-mongoid', '>= 2.0', require: false gem 'kaminari-mongoid' + gem 'mongoid', '~> 7.0' gem 'mongoid-paperclip', '>= 0.0.8', require: 'mongoid_paperclip' - gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' - gem 'cancancan-mongoid' gem 'shrine-mongoid', '~> 1.0' end end -appraise "rails-5.2" do - gem 'rails', '~> 5.2.0' - gem 'sassc-rails', '~> 2.1' - gem 'devise', '~> 4.4' - - group :test do - gem 'cancancan', '~> 2.0' - gem 'paperclip', ['>= 3.4', '!= 4.3.0'] - gem 'shrine', '~> 3.0' - end +appraise 'rails-7.0' do + gem 'rails', '~> 7.0.0' + gem 'importmap-rails', require: false group :active_record do - gem 'pg', '>= 1.0.0', platforms: :ruby - gem 'paper_trail', '>= 5.0' - platforms :jruby do - gem 'activerecord-jdbcmysql-adapter', '~> 52.0' - gem 'activerecord-jdbcpostgresql-adapter', '~> 52.0' - gem 'activerecord-jdbcsqlite3-adapter', '~> 52.0' + gem 'activerecord-jdbcmysql-adapter', '~> 70.0' + gem 'activerecord-jdbcpostgresql-adapter', '~> 70.0' + gem 'activerecord-jdbcsqlite3-adapter', '~> 70.0' end end group :mongoid do - gem 'mongoid', '~> 7.0' + gem 'cancancan-mongoid' + gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' + gem 'database_cleaner-mongoid', '>= 2.0', require: false gem 'kaminari-mongoid' + gem 'mongoid', '~> 8.0' gem 'mongoid-paperclip', '>= 0.0.8', require: 'mongoid_paperclip' - gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' - gem 'cancancan-mongoid' gem 'shrine-mongoid', '~> 1.0' end end -appraise "rails-6.0" do - gem 'rails', '~> 6.0.0' - gem 'haml' - gem 'sassc-rails', '~> 2.1' - gem 'devise', '~> 4.7' - - group :test do - gem 'cancancan', '~> 3.0' - gem 'kt-paperclip' - gem 'rspec-rails', '>= 4.0.0.beta2' - gem 'shrine', '~> 3.0' - end - - group :active_record do - gem 'pg', '>= 1.0.0', platforms: :ruby - gem 'paper_trail', '>= 5.0' - - platforms :jruby do - gem 'activerecord-jdbcmysql-adapter', '~> 60.0' - gem 'activerecord-jdbcpostgresql-adapter', '~> 60.0' - gem 'activerecord-jdbcsqlite3-adapter', '~> 60.0' - end - end +appraise 'rails-7.1' do + gem 'rails', '~> 7.1.0' + gem 'importmap-rails', require: false group :mongoid do - gem 'mongoid', '~> 7.0' + gem 'cancancan-mongoid' + gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' + gem 'database_cleaner-mongoid', '>= 2.0', require: false gem 'kaminari-mongoid' + gem 'mongoid', '~> 8.0' gem 'mongoid-paperclip', '>= 0.0.8', require: 'mongoid_paperclip' - gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' - gem 'cancancan-mongoid' gem 'shrine-mongoid', '~> 1.0' end end -appraise "rails-6.1" do - gem 'rails', '~> 6.1.0' - gem 'haml' - gem 'sassc-rails', '~> 2.1' - gem 'devise', '~> 4.7' - gem 'webrick', '~> 1.7' +appraise 'rails-7.2' do + gem 'rails', '~> 7.2.0' + gem 'importmap-rails', require: false +end - group :test do - gem 'cancancan', '~> 3.2' - gem 'kt-paperclip' - gem 'rspec-rails', '>= 4.0.0.beta2' - gem 'shrine', '~> 3.0' - end +appraise 'composite_primary_keys' do + gem 'rails', '~> 7.0.0' group :active_record do - gem 'pg', '>= 1.0.0', platforms: :ruby - gem 'paper_trail', '>= 5.0' + gem 'composite_primary_keys' end end diff --git a/CHANGELOG.md b/CHANGELOG.md index 37053e7fea..da3e7f896d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,385 +1,802 @@ # RailsAdmin Changelog -## [Unreleased](https://github.com/sferik/rails_admin/tree/HEAD) +## [Unreleased](https://github.com/railsadminteam/rails_admin/tree/HEAD) -[Full Changelog](https://github.com/sferik/rails_admin/compare/v2.1.1...HEAD) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.2.0...HEAD) +## [3.2.0](https://github.com/railsadminteam/rails_admin/tree/v3.2.0) - 2024-09-08 -## [2.1.1](https://github.com/sferik/rails_admin/tree/v2.1.1) - 2021-03-14 +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.2.0.rc...v3.2.0) -[Full Changelog](https://github.com/sferik/rails_admin/compare/v2.1.0...v2.1.1) +### Fixed + +- Fix polymorphic id doesn't reset properly in an edge case ([7b2ffb1](https://github.com/railsadminteam/rails_admin/commit/7b2ffb12386e06a0e6e0bace6d331fc5af989e38), [#3630](https://github.com/railsadminteam/rails_admin/pull/3630)) + +## [3.2.0.rc](https://github.com/railsadminteam/rails_admin/tree/v3.2.0.rc) - 2024-08-25 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.2.0.beta...v3.2.0.rc) + +### Added + +- ActiveRecord 7.1 composite primary keys support ([53b89c9](https://github.com/railsadminteam/rails_admin/commit/53b89c9161e48c0f9b4ecd5f544398c9360ea50f)) + +### Changed + +- Introduce SingularAssociation and CollectionAssociation to tidy up the view files ([876be11](https://github.com/railsadminteam/rails_admin/commit/876be11ec01237596b2f27e15239e86418ce7610)) ### Fixed -- Fix AbstractObject's proxying was incompatible with keyword arguments in Ruby 3.0 ([#3342](https://github.com/sferik/rails_admin/issues/3342)) +- Fix to show a thumbnail of all representable ActiveStorage attachments ([#3656](https://github.com/railsadminteam/rails_admin/pull/3656), [7754ac3](https://github.com/railsadminteam/rails_admin/commit/7754ac34eb8e0af7605b2e52ae0646b17e9bb0c6)) +- Fix to detect images properly in FileUpload and MultipleFileUpload field ([35c8702](https://github.com/railsadminteam/rails_admin/commit/35c8702351aa300bddcc950d36d68b80742f5011), [#3633](https://github.com/railsadminteam/rails_admin/pull/3633)) +- Fix to reset polymorphic id selection upon type change ([#3630](https://github.com/railsadminteam/rails_admin/pull/3630), [13114e5](https://github.com/railsadminteam/rails_admin/commit/13114e5629d49eab14d58df1319eb068dacedba7)) +- Lock jQuery UI version due to incompatibility with 1.14 ([5245d5b](https://github.com/railsadminteam/rails_admin/commit/5245d5bb91691d646219b5243f3f881a0144a3fd), [#3692](https://github.com/railsadminteam/rails_admin/issues/3692)) -## [2.1.0](https://github.com/sferik/rails_admin/tree/v2.1.0) - 2021-02-28 +## [3.2.0.beta](https://github.com/railsadminteam/rails_admin/tree/v3.2.0.beta) - 2024-07-13 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v2.0.2...v2.1.0) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.4...v3.2.0.beta) ### Added -- Ability to set default filter operator for fields ([#3318](https://github.com/sferik/rails_admin/pull/3318)) -- Shrine 3.x support ([#3257](https://github.com/sferik/rails_admin/pull/3257)) -- Rails 6.1 compatibility ([f0c46f1e](https://github.com/sferik/rails_admin/commit/f0c46f1e128b5d31d812ff3a80d15db8692c848b)) + +- Allow turbo-rails 2 in gemspec ([#3671](https://github.com/railsadminteam/rails_admin/pull/3671)) +- ViteRuby integration ([#3643](https://github.com/railsadminteam/rails_admin/pull/3643), [0e12e5b](https://github.com/railsadminteam/rails_admin/commit/0e12e5b465997c14d5b7a4d500a0d4cebed21aa9)) +- Support client-side dynamic scoping ([12715f2](https://github.com/railsadminteam/rails_admin/commit/12715f2dd12d97f0676e548e4271906df424b89d), [#2934](https://github.com/railsadminteam/rails_admin/issues/2934)) +- Add support for `%-l` option to Flatpickr ([#3616](https://github.com/railsadminteam/rails_admin/pull/3616)) + +### Changed + +- Handle has_one assignment on the field level, making patched has_one getters/setters unnecessary ([91737ab](https://github.com/railsadminteam/rails_admin/commit/91737ab3c2fa22cbe08aedd28770a12704fde6c7)) + +### Fixed + +- Use require_relative to avoid modifying $LOAD_PATH in gemspec ([#3690](https://github.com/railsadminteam/rails_admin/pull/3690)) +- Tidy up trailing whitespace in gem post_install_message ([#3689](https://github.com/railsadminteam/rails_admin/pull/3689)) +- Fix enum filter breaking when pre-populated ([d62f604](https://github.com/railsadminteam/rails_admin/commit/d62f604cc8d7d1434f7dfe0c5aca3aaf3dc2547b), [#3651](https://github.com/railsadminteam/rails_admin/issues/3651)) +- Fix error on searching or sorting by ActiveStorage field ([dba6c4b](https://github.com/railsadminteam/rails_admin/commit/dba6c4b815fbe4aa4f62a13b660e865a89151838), [#3678](https://github.com/railsadminteam/rails_admin/issues/3678)) +- Fix to remove trailing slash from meta tags ([#3672](https://github.com/railsadminteam/rails_admin/pull/3672)) +- Fix default config path for ImportmapFormatter being misspelled ([#3676](https://github.com/railsadminteam/rails_admin/pull/3676)) +- Fix table names not quoted properly on sorting ([#3652](https://github.com/railsadminteam/rails_admin/pull/3652), [#1631](https://github.com/railsadminteam/rails_admin/issues/1631)) +- Fix ActiveStorage/ActionText detection less likely to cause false positives ([073b809](https://github.com/railsadminteam/rails_admin/commit/073b809853b6bc231841e3f8dd9d35875220c616), [#3659](https://github.com/railsadminteam/rails_admin/issues/3659)) +- Fix boolean fields in a Mongoid embedded document fails to be updated ([#3555](https://github.com/railsadminteam/rails_admin/pull/3555), [#3554](https://github.com/railsadminteam/rails_admin/issues/3554)) +- Fix wrongly referring to `:update_only` in nested fields ([#3649](https://github.com/railsadminteam/rails_admin/pull/3649)) +- Fix to use HTML `q` element for better localization support ([#3636](https://github.com/railsadminteam/rails_admin/pull/3636)) +- Fix `is_blank` and `is_present` filters breaking for uuid columns ([#3629](https://github.com/railsadminteam/rails_admin/pull/3629), [#3669](https://github.com/railsadminteam/rails_admin/issues/3669)) +- Fix polymorphic association target classes not set correctly in subclasses ([2a89ebc](https://github.com/railsadminteam/rails_admin/commit/2a89ebcfa96243697988f6570b9c9be19a7a01b5), [#3631](https://github.com/railsadminteam/rails_admin/issues/3631)) + +### Security + +- Validate `return_to` param using `request.base_url` to prevent arbitrary redirection ([#3627](https://github.com/railsadminteam/rails_admin/pull/3627)) + +## [3.1.4](https://github.com/railsadminteam/rails_admin/tree/v3.1.4) - 2024-07-09 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.3...v3.1.4) + +### Fixed + +- Fix [32f91e4](https://github.com/railsadminteam/rails_admin/commit/32f91e4b49205e44d3931c2e36d9f7273384a250) broke fields with HTML tags ([758d249](https://github.com/railsadminteam/rails_admin/commit/758d249d950062be6840f9c96e2a286e02b92a1e), [#3686](https://github.com/railsadminteam/rails_admin/issues/3686#issuecomment-2215491140)) + +## [3.1.3](https://github.com/railsadminteam/rails_admin/tree/v3.1.3) - 2024-07-06 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.2...v3.1.3) ### Fixed -- Some translation entries of filtering-multiselect weren't localizable ([#3315](https://github.com/sferik/rails_admin/pull/3315)) -- Thumbnail generation breaks when used with ActiveStorage 6.x and ruby-vips ([#3255](https://github.com/sferik/rails_admin/pull/3255), [2dba791c](https://github.com/sferik/rails_admin/commit/2dba791c9135b3202d662f90fac443d282869bd6)) -- Hide present/blank filter options for required fields ([#3340](https://github.com/sferik/rails_admin/pull/3340)) -- Fix to show correct filename for multiple attachments ([#3295](https://github.com/sferik/rails_admin/pull/3295)) -- Fix encoding detection was incompatible with DB connection proxies like active_record_host_pool gem ([#3313](https://github.com/sferik/rails_admin/pull/3313)) -- Fix hidden fields breaking indentation ([#3278](https://github.com/sferik/rails_admin/pull/3278), [#2487](https://github.com/sferik/rails_admin/issues/2487)) + +- Fix bson 5.0 compatibility ([13da4f0](https://github.com/railsadminteam/rails_admin/commit/13da4f0191f5185dadecdfe9e6fc9ca808f7f73d)) +- Fix Importmap 2.0 compatibility ([bd0cf97](https://github.com/railsadminteam/rails_admin/commit/bd0cf97530d93dc66577e5d1ea0da2ebf3b57737)) + +### Security + +- Fix XSS vulnerability in the list view ([b5a287d](https://github.com/railsadminteam/rails_admin/commit/b5a287d82e2cbd1737a1a01e11ede2911cce7fef), [GHSA-8qgm-g2vv-vwvc](https://github.com/railsadminteam/rails_admin/security/advisories/GHSA-8qgm-g2vv-vwvc)) + +## [3.1.2](https://github.com/railsadminteam/rails_admin/tree/v3.1.2) - 2023-03-23 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.1...v3.1.2) + +### Fixed + +- Fix install failing with importmap setup ([aca22b6](https://github.com/railsadminteam/rails_admin/commit/aca22b6ba1eca1ac618525334cf14fc946e1c99e), [#3609](https://github.com/railsadminteam/rails_admin/issues/3609)) +- Fix to show non-eager-loaded models which are explicitly configured ([87c9d5b](https://github.com/railsadminteam/rails_admin/commit/87c9d5bc5b6ffb423e72054b3cfe8f949c12c178), [#3604](https://github.com/railsadminteam/rails_admin/issues/3604)) +- Fix `rails_admin.dom_ready` event not triggered with jQuery `on` ([2ee43de](https://github.com/railsadminteam/rails_admin/commit/2ee43deb1fa8d3a9e3ea0e589c1687d684e19ad6), [33773d7](https://github.com/railsadminteam/rails_admin/commit/33773d7f8dd43eeb0f6a7c125c4bee170132e5d2), [#3600](https://github.com/railsadminteam/rails_admin/discussions/3600)) +- Restore caching in RailsAdmin::Config::Model#excluded? ([#3587](https://github.com/railsadminteam/rails_admin/pull/3587)) +- Optimize/simplify viable_models file path to class name logic ([#3589](https://github.com/railsadminteam/rails_admin/pull/3589)) + +## [3.1.1](https://github.com/railsadminteam/rails_admin/tree/v3.1.1) - 2022-12-18 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.0...v3.1.1) + +### Changed + +- Relax Font-Awesome dependency to allow Webpacker users to stay on 5.x ([3a7f348](https://github.com/railsadminteam/rails_admin/commit/3a7f34875248e446b48fd76870f0c337fee6dcf9), [#3565](https://github.com/railsadminteam/rails_admin/issues/3565)) ### Removed -- Remove `yell_for_non_accessible_fields` option since it has no effect since 0.5.0 ([#3249](https://github.com/sferik/rails_admin/pull/3249)) +- Remove unused glphyicon assets ([#3578](https://github.com/railsadminteam/rails_admin/pull/3578)) + +### Fixed + +- Simplify uses of defined? ([#3561](https://github.com/railsadminteam/rails_admin/pull/3561)) +- Define jQuery object in separate file to support esbuild ([#3571](https://github.com/railsadminteam/rails_admin/pull/3571)) +- Fix filter box being duplicated on browser back ([c6b1893](https://github.com/railsadminteam/rails_admin/commit/c6b18934cff3db0768836d799ee1bea54045709c), [#3570](https://github.com/railsadminteam/rails_admin/issues/3570)) +- Fix sidebar menu expanding horizontally, preventing vertical scroll ([9997c10](https://github.com/railsadminteam/rails_admin/commit/9997c1095066aaac39afb27bf8de705cf6ccb1ef), [#3564](https://github.com/railsadminteam/rails_admin/issues/3564)) + +## [3.1.0](https://github.com/railsadminteam/rails_admin/tree/v3.1.0) - 2022-11-06 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.0.rc2...v3.1.0) + +### Fixed + +- Fix to use defer instead of async to ensure script loading order ([2a40976](https://github.com/railsadminteam/rails_admin/commit/2a409764b13a4e23fc848725c604d318e3375484), [#3513](https://github.com/railsadminteam/rails_admin/issues/3513)) +- Improve filter method select box appearance ([#3559](https://github.com/railsadminteam/rails_admin/pull/3559)) + +## [3.1.0.rc2](https://github.com/railsadminteam/rails_admin/tree/v3.1.0.rc2) - 2022-10-02 -## [2.0.2](https://github.com/sferik/rails_admin/tree/v2.0.2) - 2020-03-17 +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.0.rc...v3.1.0.rc2) + +### Fixed + +- Fix sidebar style broken in attempt to support Bootstrap 5.2 ([d7abba4](https://github.com/railsadminteam/rails_admin/commit/d7abba4e8a2e380b4d7f8fb3b37302300af63de5), [#3553](https://github.com/railsadminteam/rails_admin/issues/3553)) + +## [3.1.0.rc](https://github.com/railsadminteam/rails_admin/tree/v3.1.0.rc) - 2022-09-22 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.1.0.beta...v3.1.0.rc) + +### Added + +- Add ability to limit filter operators ([be9a75e](https://github.com/railsadminteam/rails_admin/commit/be9a75e504edd9a754157a4ddba590e8a5d14149)) +- Support filtering has_one associations ([9657774](https://github.com/railsadminteam/rails_admin/commit/9657774b423912357d8ad8a476b644bb4e36dc30)) +- Add ability to set default order of PaperTrail versions ([a1c4c67](https://github.com/railsadminteam/rails_admin/commit/a1c4c673041642ae2a2aa07e3f0555e17be9d940), [#3095](https://github.com/railsadminteam/rails_admin/pull/3095)) +- Add block-style DSL support for extension adapters ([951b708](https://github.com/railsadminteam/rails_admin/commit/951b70878cb007d54b4a7aeadd708d4c7668727b)) +- Make sidebar navigation collapsible ([e8cb8ed](https://github.com/railsadminteam/rails_admin/commit/e8cb8edd39246bf75ed72295b7832faf5056367c), [#3198](https://github.com/railsadminteam/rails_admin/pull/3198)) +- Add ability to show a help text under the search box ([94f16fb](https://github.com/railsadminteam/rails_admin/commit/94f16fb5fdfdaa867173e95820583a68e5a306c5)) +- Support for ActiveStorage direct uploads ([e13c7e2](https://github.com/railsadminteam/rails_admin/commit/e13c7e23f789ed2a575eff01b75e52a41cc930b7), [#3296](https://github.com/railsadminteam/rails_admin/issues/3296)) + +### Changed + +- Load ActionText assets statically to enable full-featured setup ([458d0fb](https://github.com/railsadminteam/rails_admin/commit/458d0fb56d79d4fa0a252ad1ca715fd0a3b4b900), [#3251](https://github.com/railsadminteam/rails_admin/issues/3251), [#3386](https://github.com/railsadminteam/rails_admin/issues/3386)) +- Change ES Module processing not to affect non-RailsAdmin assets ([f8219bf](https://github.com/railsadminteam/rails_admin/commit/f8219bf3bc508c5e42f5d044cf6355a56726f8b2)) +- Change RailsAdmin initialization process to evaluate the config block immediately but load constants lazily ([#3541](https://github.com/railsadminteam/rails_admin/pull/3541), [33e9214](https://github.com/railsadminteam/rails_admin/commit/33e92147214975cddace61737cbbde3117663efe)) +- Restore the loading indicator back ([32e6b14](https://github.com/railsadminteam/rails_admin/commit/32e6b1463ecf66b38e6fdc7924426d9753d71eab), [b9ee7f0](https://github.com/railsadminteam/rails_admin/commit/b9ee7f0c202abc7c09726bdaa28536e7ec25127e), [#3500](https://github.com/railsadminteam/rails_admin/issues/3500)) + +### Fixed + +- Fix Bootstrap 5.2 compatibility ([ef76fce](https://github.com/railsadminteam/rails_admin/commit/ef76fcea0b23aed04f6568448cd157ccc9ba30a0)) +- Fix filtering select widget options being empty on browser back ([3cffc00](https://github.com/railsadminteam/rails_admin/commit/3cffc0002079bc9d515dc0f3b31513c7166aa2b9)) +- Fix RailsAdmin widgets not activated after a validation error ([a604da5](https://github.com/railsadminteam/rails_admin/commit/a604da5670378e57da7b5492afad40e4f4ad083d)) +- Fix export action didn't use export section for eager loading ([4cc3f30](https://github.com/railsadminteam/rails_admin/commit/4cc3f304cc0bc4a7dbaf41a6e4e12ecbdbba6e22), [#1954](https://github.com/railsadminteam/rails_admin/issues/1954)) +- Fix Dart Sass 2.0 division deprecations ([#3544](https://github.com/railsadminteam/rails_admin/pull/3544), [3a177c2](https://github.com/railsadminteam/rails_admin/commit/3a177c2e8c1b1d782186ee5bbb3c0d44cc4c0060)) +- Fix unable to focus elements on modals opened from remote forms ([#3539](https://github.com/railsadminteam/rails_admin/pull/3539), [#3538](https://github.com/railsadminteam/rails_admin/issues/3538)) +- Improve pagination appearance on smaller screens ([a2e366e](https://github.com/railsadminteam/rails_admin/commit/a2e366e8839c9046b9f9fde219875a05bc1a66bb), [#3516](https://github.com/railsadminteam/rails_admin/issues/3516)) +- Fix the value of submit buttons being lost on submission ([60e1150](https://github.com/railsadminteam/rails_admin/commit/60e115053ea34fa42c3099464106cf6e58dbfa03), [#3513](https://github.com/railsadminteam/rails_admin/issues/3513)) +- Fix breaking with a has_and_belongs_to_many association with scope given ([c2bf6db](https://github.com/railsadminteam/rails_admin/commit/c2bf6db41a97b46741bc41b56fd9700c8bdfc9e4), [#2067](https://github.com/railsadminteam/rails_admin/issues/2067)) +- Fix nested fields don't toggle properly after pushing 'Add a new ...' button ([d1f1154](https://github.com/railsadminteam/rails_admin/commit/d1f115425c4a8d119fdeb37802fe472ae278918d), [#3528](https://github.com/railsadminteam/rails_admin/issues/3528)) + +## [3.1.0.beta](https://github.com/railsadminteam/rails_admin/tree/v3.1.0.beta) - 2022-06-20 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.0.0...v3.1.0.beta) + +### Added + +- Support Importmap and Webpack, via [importmap-rails](https://github.com/rails/importmap-rails) / [jsbundling-rails](https://github.com/rails/jsbundling-rails) and [cssbundling-rails](https://github.com/rails/cssbundling-rails) ([#3488](https://github.com/railsadminteam/rails_admin/pull/3488)) +- Support [Composite Primary Keys](https://github.com/composite-primary-keys/composite_primary_keys) gem ([#3527](https://github.com/railsadminteam/rails_admin/pull/3527)) +- Allow configuration of Navbar css class ([126f7ac](https://github.com/railsadminteam/rails_admin/commit/126f7ac79b2ce7846cbbed7bbca1cc26dfe46fb6), [#3507](https://github.com/railsadminteam/rails_admin/issues/3507)) + +### Changed + +- Update vendorized jQuery to 3.6.0 ([#3524](https://github.com/railsadminteam/rails_admin/pull/3524)) +- Enable frozen string literals across the project ([#3483](https://github.com/railsadminteam/rails_admin/pull/3483)) + +### Fixed + +- Fix edit user link in the top navigation pointing to wrong URL ([#3531](https://github.com/railsadminteam/rails_admin/pull/3531)) +- Fix MultipleActiveStorage field deleting previous attachments when updating a record in Rails 7.0 ([974c54a](https://github.com/railsadminteam/rails_admin/commit/974c54a2a3372690ca189f6e7e90ce365bcd4ff5), [#3520](https://github.com/railsadminteam/rails_admin/issues/3520)) +- Fix remote form submission breaking when used with HTTP/2 ([#3515](https://github.com/railsadminteam/rails_admin/pull/3515)) +- Fix to maintain 2.x hover / active behavior for side navigation links ([#3511](https://github.com/railsadminteam/rails_admin/pull/3511)) +- Fix default sort by behavior when `list.sort_by` points to a field with a table reference for `:sortable` ([#3509](https://github.com/railsadminteam/rails_admin/pull/3509), [9959925](https://github.com/railsadminteam/rails_admin/commit/9959925e49812d6fdaf7341ede4b1d66d926e8d8)) +- Fix to insert whitespace after sidebar navigation icon to maintain visual consistency ([#3504](https://github.com/railsadminteam/rails_admin/pull/3504)) +- Fix orderable multiselect buttons not rendered correctly ([#3506](https://github.com/railsadminteam/rails_admin/pull/3506)) +- Fix to use badges instead of labels, which are removed in Bootstrap 5 ([#3503](https://github.com/railsadminteam/rails_admin/pull/3503)) + +## [3.0.0](https://github.com/railsadminteam/rails_admin/tree/v3.0.0) - 2022-03-21 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.0.0.rc4...v3.0.0) + +### Fixed + +- Fix table sorting not working ([83a0c88](https://github.com/railsadminteam/rails_admin/commit/83a0c889f1de38a0b24c412b0f0f7484add86b29), [#3497](https://github.com/railsadminteam/rails_admin/issues/3497)) +- Fix reset button by the query box not working ([4a583e9](https://github.com/railsadminteam/rails_admin/commit/4a583e924c5bef6bfc46d6a49ab751c2323ba23e)) + +## [3.0.0.rc4](https://github.com/railsadminteam/rails_admin/tree/v3.0.0.rc4) - 2022-03-13 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.0.0.rc3...v3.0.0.rc4) + +### Added + +- Instruct users on potential issues with npm package installation ([2b0594c](https://github.com/railsadminteam/rails_admin/commit/2b0594c6824eb4eed217cbab9477639b61eb99db), [c7a74f1](https://github.com/railsadminteam/rails_admin/commit/c7a74f1ce7a040b8c87b24fe1907ddd9088bf1e5)) + +### Changed + +- Upgrade vendorized Flatpickr to 4.6.11 ([7f8c831](https://github.com/railsadminteam/rails_admin/commit/7f8c831b9aa407987ba89191c21040e2e72e366e)) + +### Fixed + +- Fix not utilizing full browser width after Bootstrap 5 upgrade ([#3493](https://github.com/railsadminteam/rails_admin/pull/3493)) +- Fix the style for show views broken on Bootstrap 5 upgrade ([#3491](https://github.com/railsadminteam/rails_admin/pull/3491)) +- Fix Pundit 2.2 deprecation for not using Pundit::Authorization ([e38eb46](https://github.com/railsadminteam/rails_admin/commit/e38eb46ffe79cd866c34b837dd4cfbb65361558f)) +- Fix JS issues when navigating across the main app and RailsAdmin ([eb4a185](https://github.com/railsadminteam/rails_admin/commit/eb4a18558e9d8c69aeea3fd733f5dc251f3f79f9), [#3484](https://github.com/railsadminteam/rails_admin/pull/3484)) + +## [3.0.0.rc3](https://github.com/railsadminteam/rails_admin/tree/v3.0.0.rc3) - 2022-02-27 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.0.0.rc2...v3.0.0.rc3) + +### Fixed + +- Fix the style of list scope tabs ([#3477](https://github.com/railsadminteam/rails_admin/pull/3477)) +- Fix rake tasks executed twice ([7d56cd6](https://github.com/railsadminteam/rails_admin/commit/7d56cd6af2d468cca44af5211559af0e744f5cb4)) +- Fix the style of the header user link when show_gravatar is false ([#3475](https://github.com/railsadminteam/rails_admin/pull/3475)) +- Fix 'Cancel' button for delete/bulk_delete action also didn't work ([1fa8486](https://github.com/railsadminteam/rails_admin/commit/1fa8486ead39596e9e0ac62a945fb0aed63929a8), [#3468](https://github.com/railsadminteam/rails_admin/issues/3468)) +- Fix failing to export after introducing Turbo Drive ([c749d93](https://github.com/railsadminteam/rails_admin/commit/c749d939e29434937e8558bcb1f3e219fe98c69d), [#3461 (comment)](https://github.com/railsadminteam/rails_admin/pull/3461#issuecomment-1048588801)) + +## [3.0.0.rc2](https://github.com/railsadminteam/rails_admin/tree/v3.0.0.rc2) - 2022-02-20 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.0.0.rc...v3.0.0.rc2) + +### Added -[Full Changelog](https://github.com/sferik/rails_admin/compare/v2.0.1...v2.0.2) +- Some improvements to make the upgrade process more friendlier ([#3471](https://github.com/railsadminteam/rails_admin/pull/3471), [3771542](https://github.com/railsadminteam/rails_admin/commit/377154268257d3753093dcd3dd2fd79b7438b9aa), [3b04a96](https://github.com/railsadminteam/rails_admin/commit/3b04a9616169d568b1a6cee26becf9bfea0ee386)) ### Fixed -- Fix to use I18n to translate the button 'Reset filters'([#3248](https://github.com/sferik/rails_admin/pull/3248)) + +- Fix 'Save and add another', 'Save and edit', 'Cancel' buttons didn't work right ([ac0a563](https://github.com/railsadminteam/rails_admin/commit/ac0a563a0bf91bf33c693e19717148ed77f843fd), [#3468](https://github.com/railsadminteam/rails_admin/issues/3468)) +- Fix failing to precompile assets when the database connection is unavailable ([#3470](https://github.com/railsadminteam/rails_admin/pull/3470), [#3469](https://github.com/railsadminteam/rails_admin/issues/3469)) +- Fix custom theme overrides not working ([3d7f3b3](https://github.com/railsadminteam/rails_admin/commit/3d7f3b33a1ca6fabbd8606bb178babae930cce25), [#3466](https://github.com/railsadminteam/rails_admin/issues/3466)) + +## [3.0.0.rc](https://github.com/railsadminteam/rails_admin/tree/v3.0.0.rc) - 2022-02-06 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.0.0.beta2...v3.0.0.rc) + +### Added + +- Support Mongoid's Storage Field Names ([cefa23c](https://github.com/railsadminteam/rails_admin/commit/cefa23c9d23d06dc1134228e142e6f0aa4655c54), [#1745](https://github.com/railsadminteam/rails_admin/issues/1745)) +- Allow save/delete operations to be disabled based on an object's `#read_only?` status ([9cd7541](https://github.com/railsadminteam/rails_admin/commit/9cd7541a2e6af4ae4941b200840a1474baeb2f06), [#1684](https://github.com/railsadminteam/rails_admin/issues/1684)) +- Allow customizing model's last created time ([d6d380a](https://github.com/railsadminteam/rails_admin/commit/d6d380a02e955c3b14ff7dc30f1809a2a6cd0f47), [#3010](https://github.com/railsadminteam/rails_admin/issues/3010)) +- Add ability to hide the dashboard history section ([#3189](https://github.com/railsadminteam/rails_admin/pull/3189)) +- Add model scope configuration option, which enables 'unscoped' mode ([8d905f9](https://github.com/railsadminteam/rails_admin/commit/8d905f9e2f1102e8addf324b496720e3fd47bf1d), [#1348](https://github.com/railsadminteam/rails_admin/issues/1348)) + +### Changed + +- Switch from pjax to Turbo Drive, due to pjax's low maintenance activity ([#3461](https://github.com/railsadminteam/rails_admin/pull/3461), [#3435](https://github.com/railsadminteam/rails_admin/pull/3435)) +- Upgrade Bootstrap to 5.1.3 ([#3455](https://github.com/railsadminteam/rails_admin/pull/3455), [#3083](https://github.com/railsadminteam/rails_admin/issues/3083)) +- Switch datetime picker library to Flatpickr ([#3455](https://github.com/railsadminteam/rails_admin/pull/3455)) + +### Removed + +- Drop support for Ruby 2.5 ([#3430](https://github.com/railsadminteam/rails_admin/pull/3430)) +- Remove Sections::List#sort_reverse because of having very limited usecase ([0c7bc61](https://github.com/railsadminteam/rails_admin/commit/0c7bc6124a189e5307f2bd1f960dd06495932d10), [#1181](https://github.com/railsadminteam/rails_admin/issues/1181)) + +### Fixed + +- Fix failing to detect encoding with JDBC MySQL adapter ([0dfe2e4](https://github.com/railsadminteam/rails_admin/commit/0dfe2e4d1301cc353f972b28ab19554a53cad482)) +- Fix unable to start app when using redis-session-store ([#3462](https://github.com/railsadminteam/rails_admin/pull/3462)) +- Fix ActiveRecord ObjectExtension has_one setter breaks with custom primary key class ([0e2e0e4](https://github.com/railsadminteam/rails_admin/commit/0e2e0e4e24328a063813a3a5266a15faee7960c8), [#3460](https://github.com/railsadminteam/rails_admin/issues/3460)) +- Fix inheritance of parent_controller not updated properly when controllers were eagerly loaded ([#3458](https://github.com/railsadminteam/rails_admin/pull/3458)) +- Fix to retrieve actions correctly in the action `#bulk_action` ([#3407](https://github.com/railsadminteam/rails_admin/pull/3407)) +- Fix issue when RailsAdmin::MainController needs to dispatch a method call using `#respond_to_missing?` ([da51b91](https://github.com/railsadminteam/rails_admin/commit/da51b91f712b235c0c6e2a120724fcb31ff28168), [#3454](https://github.com/railsadminteam/rails_admin/issues/3454)) +- Fix modal foreign keys are not prepopulated unless the association inverse_of is configured ([75504d0](https://github.com/railsadminteam/rails_admin/commit/75504d08ee6197a3a2c72a56ea30f01272b1d28b), [#2585](https://github.com/railsadminteam/rails_admin/issues/2585)) + +## [3.0.0.beta2](https://github.com/railsadminteam/rails_admin/tree/v3.0.0.beta2) - 2021-12-25 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v3.0.0.beta...v3.0.0.beta2) + +### Fixed + +- Fix NameError 'uninitialized constant RailsAdmin::Version' on install ([d2ad520](https://github.com/railsadminteam/rails_admin/commit/d2ad5209f98d8678c6317b49f13727b8605048b3), [#3452](https://github.com/railsadminteam/rails_admin/issues/3452)) + +## [3.0.0.beta](https://github.com/railsadminteam/rails_admin/tree/v3.0.0.beta) - 2021-12-20 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.2.1...v3.0.0.beta) + +### Added + +- Rails 7.0.0 support ([011b9ae](https://github.com/railsadminteam/rails_admin/commit/011b9aea12d2c3a5e8654786e49071254baacf18), [670d803](https://github.com/railsadminteam/rails_admin/commit/670d803c262429600c6eb7f59f8b36aa145081e5)) +- Webpacker support ([#3414](https://github.com/railsadminteam/rails_admin/pull/3414)) +- Add #link_target to action configuration ([#3419](https://github.com/railsadminteam/rails_admin/pull/3419)) +- Add not like (=does not contain) operator ([#3410](https://github.com/railsadminteam/rails_admin/pull/3410)) +- Support for PostgreSQL citext data type ([#3413](https://github.com/railsadminteam/rails_admin/pull/3413), [#2177](https://github.com/railsadminteam/rails_admin/issues/2177)) +- Allow #configure to handle multiple fields for a section at once ([#3406](https://github.com/railsadminteam/rails_admin/pull/3406), [#2667](https://github.com/railsadminteam/rails_admin/issues/2667)) +- Add has_one id setters/getters, eliminating the need for explicitly defining them ([42f0a5f](https://github.com/railsadminteam/rails_admin/commit/42f0a5f5aac4b7481dcd4950803d12077b598ce5), [#2625](https://github.com/railsadminteam/rails_admin/issues/2625)) +- Support for Mongoid's has_and_belongs_to_many custom primary_key ([3f67637](https://github.com/railsadminteam/rails_admin/commit/3f676371ce7fc088220095afa12d3e20c2c6123a), [#3097](https://github.com/railsadminteam/rails_admin/pull/3097)) +- Support for eager-loading arbitrary associations ([4404758](https://github.com/railsadminteam/rails_admin/commit/44047580b7d536a84a92c173a3bd00dc9d211399), [#2928](https://github.com/railsadminteam/rails_admin/issues/2928)) +- Support for nullable boolean field ([7583369](https://github.com/railsadminteam/rails_admin/commit/7583369a1d1763e542d36930a968726425cacb6c), [#3145](https://github.com/railsadminteam/rails_admin/issues/3145)) +- Support for configuration reload in development mode ([e4ae669](https://github.com/railsadminteam/rails_admin/commit/e4ae6698e52e56a1cefdf5c4097b29b8306f21e2), [#2726](https://github.com/railsadminteam/rails_admin/issues/2726), [08f50aa](https://github.com/railsadminteam/rails_admin/commit/08f50aabc1922aeb289ced4ccbe9f983bc1aaa89), [#3420](https://github.com/railsadminteam/rails_admin/issues/3420)) +- Add 'No objects found' placeholder in filtering-select as well ([7e3a1a6](https://github.com/railsadminteam/rails_admin/commit/7e3a1a632811a917f2cbd8dbe471861bc0a1ac85), [#3332](https://github.com/railsadminteam/rails_admin/issues/3332)) +- Add inline_edit to HasManyAssociation as well ([798ab1b](https://github.com/railsadminteam/rails_admin/commit/798ab1b6ae400f2b853441a9fa48f8e7ad24208d), [#1911](https://github.com/railsadminteam/rails_admin/issues/1911)) +- Add hover highlight to the list table for better visibility ([#3221](https://github.com/railsadminteam/rails_admin/pull/3221)) +- Add ability to show disabled actions, as well as completely hiding ([6c877ea](https://github.com/railsadminteam/rails_admin/commit/6c877eac1a052881b2d4950a44d41f7fd77b044f), [#1765](https://github.com/railsadminteam/rails_admin/issues/1765)) +- Add the message 'no records found' when a list is empty ([#3365](https://github.com/railsadminteam/rails_admin/pull/3365), [a5fe6f8](https://github.com/railsadminteam/rails_admin/commit/a5fe6f806498975a91ae11544ce21310592774a2), [#3329](https://github.com/railsadminteam/rails_admin/issues/3329)) +- Add a way to clear belongs_to selection using mouse ([ac3fe35](https://github.com/railsadminteam/rails_admin/commit/ac3fe35b65e858a3b528a523e1b6fbeafe1d359b), [#2090](https://github.com/railsadminteam/rails_admin/issues/2090)) +- Add HTML5 validation for float-like field types ([#3378](https://github.com/railsadminteam/rails_admin/pull/3378), [#3289](https://github.com/railsadminteam/rails_admin/issues/3289)) + +### Changed + +- Remove horizontal pagination and always use sidescroll view for list action table ([d51e943](https://github.com/railsadminteam/rails_admin/commit/d51e94314de4b510df0767a695d5953bb7b3fad5)) +- Replace image assets with Font Awesome icons ([a0a568b](https://github.com/railsadminteam/rails_admin/commit/a0a568bc866158382b52ec24639699695146794c)) +- Switch templates from HAML to ERB ([#3425](https://github.com/railsadminteam/rails_admin/pull/3425), [#3439](https://github.com/railsadminteam/rails_admin/pull/3439), [#3173](https://github.com/railsadminteam/rails_admin/issues/3173)) +- Rewrite some JavaScript code not to use jQuery ([#3416](https://github.com/railsadminteam/rails_admin/pull/3416), [#3417](https://github.com/railsadminteam/rails_admin/pull/3417)) +- Upgrade FontAwesome to 5.15.4 ([cb1ac73](https://github.com/railsadminteam/rails_admin/commit/cb1ac732c4df85cc082c4e40f2c8a7d144ceb9f2)) +- Stop using AbstractObject and use raw model instances with extension ([af88091](https://github.com/railsadminteam/rails_admin/commit/af88091d11590dc95df4866c62fba0c49a7bb9a8), [#2847](https://github.com/railsadminteam/rails_admin/issues/2847)) +- Switch from jquery_ujs to rails-ujs ([#3390](https://github.com/railsadminteam/rails_admin/pull/3390), [dea63f4](https://github.com/railsadminteam/rails_admin/commit/dea63f49ee18644ee5e8dee1fb57e0c742d27a97)) +- Make colorpicker field use HTML5 native color picker ([#3387](https://github.com/railsadminteam/rails_admin/pull/3387)) +- Change to use ISO 8601 time format for browser-server communication, instead of localized value ([01e8d5f](https://github.com/railsadminteam/rails_admin/commit/01e8d5fc8ec94e68af6fdbd80759a751cd83f74a), [#3344](https://github.com/railsadminteam/rails_admin/issues/3344)) + +### Removed + +- Remove dependency for builder and remotipart ([#3427](https://github.com/railsadminteam/rails_admin/pull/3427), [58b76d1](https://github.com/railsadminteam/rails_admin/commit/58b76d1e66ec06b752567da9c118f611105ff39f)) +- Remove capitalization helper, letting I18n to perform necessary transformation ([#3396](https://github.com/railsadminteam/rails_admin/pull/3396)) +- Remove jQuery Migrate ([#3389](https://github.com/railsadminteam/rails_admin/pull/3389), [b385d4d](https://github.com/railsadminteam/rails_admin/commit/b385d4d50f372de65d8c6c33a4b8256403894483)) +- Remove the legacy history adapter([#3374](https://github.com/railsadminteam/rails_admin/issues/3374), [b627580](https://github.com/railsadminteam/rails_admin/commit/b62758039308872e7abe91a51715e1608bfd0915)) +- Drop support for Ruby < 2.5 and Rails 5.x([decf428](https://github.com/railsadminteam/rails_admin/commit/decf4280183b8b6a453aa802942fc825524c2f13), [17e20b6](https://github.com/railsadminteam/rails_admin/commit/17e20b6daef1708598ab8cff5678501f8bac4709)) + +### Fixed + +- Reduce object allocations when rendering main navigation menu ([#3412](https://github.com/railsadminteam/rails_admin/pull/3412)) +- Fix N+1 queries for ActiveStorage attachments ([e4d5b2f](https://github.com/railsadminteam/rails_admin/commit/e4d5b2f3bece0f5f3e5f588e20e52031ad33e124), [#3282](https://github.com/railsadminteam/rails_admin/issues/3282)) +- Fix to convert DateTime format for Moment.js as much as possible ([6d5c049](https://github.com/railsadminteam/rails_admin/commit/6d5c049124d0b390f4612e8e6524f00500afe52e), [#2736](https://github.com/railsadminteam/rails_admin/issues/2736), [#3009](https://github.com/railsadminteam/rails_admin/issues/3009)) +- Fix config.parent_controller to work after the class loading ([5bd9805](https://github.com/railsadminteam/rails_admin/commit/5bd980564a565906a6fda87d2ef31590d3e3b0a5), [#2790](https://github.com/railsadminteam/rails_admin/issues/2790)) +- Fix NoMethodError when Mongoid's raise_not_found_error is false ([973bd8e](https://github.com/railsadminteam/rails_admin/commit/973bd8e50591a538841575c33b6221706481dae3), [#2623](https://github.com/railsadminteam/rails_admin/issues/2623)) +- Fix NoMethodError "undefined method 'has_one_attached'" ([e4ae669](https://github.com/railsadminteam/rails_admin/commit/e4ae6698e52e56a1cefdf5c4097b29b8306f21e2), [#3025](https://github.com/railsadminteam/rails_admin/issues/3025)) +- Fix NoMethodError "undefined method `label' for nil:NilClass" on export ([f2104b5](https://github.com/railsadminteam/rails_admin/commit/f2104b595930491a18ede44c9e1a8c881a0316b8), [#1685](https://github.com/railsadminteam/rails_admin/issues/1685)) +- Fix Kaminari's custom param_name was not used in history_index and history_show ([#3227](https://github.com/railsadminteam/rails_admin/pull/3227), [#3400](https://github.com/railsadminteam/rails_admin/pull/3400)) +- Fix Gravater and email were not shown when the current user is not editable ([bd44929](https://github.com/railsadminteam/rails_admin/commit/bd449292088cb68aad22b282b6344778862187e5), [#3237](https://github.com/railsadminteam/rails_admin/issues/3237)) +- Fix RailsAdmin::Config.reset didn't clear the effect of previous included_models/excluded_models ([1190d51](https://github.com/railsadminteam/rails_admin/commit/1190d510ee73949af618bd279e4712f1be4550b6), [#3305](https://github.com/railsadminteam/rails_admin/issues/3305)) +- Fix duplication of filtering-multiselect on browser back ([3c10b09](https://github.com/railsadminteam/rails_admin/commit/3c10b0918b65e64624f06ebd5e402181f478cc64), [#3211](https://github.com/railsadminteam/rails_admin/issues/3211)) +- Fix no error message is shown on failure with dependent: :restrict_with_error ([bf353cc](https://github.com/railsadminteam/rails_admin/commit/bf353cc76d6e56d511e9c5a87d0ffbd83669e3f2), [#3323](https://github.com/railsadminteam/rails_admin/issues/3223)) +- Fix read-only associations are shown empty if it has no value ([7580f33](https://github.com/railsadminteam/rails_admin/commit/7580f3366a6e4a16b439de7fd64860fe26628ad7), [#2681](https://github.com/railsadminteam/rails_admin/issues/2681)) +- Fix hidden fields taking up some space ([5aaee51](https://github.com/railsadminteam/rails_admin/commit/5aaee5153441fd82854a998ac02ebbe303b82bf2), [#3380](https://github.com/railsadminteam/rails_admin/issues/3380)) +- Fix to show validation errors in modals ([f67defb](https://github.com/railsadminteam/rails_admin/commit/f67defb55ad9d1a1fe8428029c947bcd00b5ce8a), [#1735](https://github.com/railsadminteam/rails_admin/issues/1735)) +- Fix image file detection by using Mime::Type ([#3398](https://github.com/railsadminteam/rails_admin/pull/3398), [#3239](https://github.com/railsadminteam/rails_admin/issues/3239)) +- Fix 'no objects' message not showing up in filtering-multiselect widget ([aa5545c](https://github.com/railsadminteam/rails_admin/commit/aa5545c928ce0de80142966c19b64be76d88286f)) +- Fix 'Delete Image' translation does not work well in some languages ([#3382](https://github.com/railsadminteam/rails_admin/pull/3382), [#3260](https://github.com/railsadminteam/rails_admin/issues/3260)) +- Fix polymorphic associations don't work with namespaced classes ([#3377](https://github.com/railsadminteam/rails_admin/pull/3377), [#3376](https://github.com/railsadminteam/rails_admin/issues/3376)) +- Fix Boolean pretty_value to include default fallback ([#3379](https://github.com/railsadminteam/rails_admin/pull/3379)) +- Fix history#index not supporting models with custom version classes ([ed19f9e](https://github.com/railsadminteam/rails_admin/commit/ed19f9e793b91de1c2bc9133e026b0396e6ec777)) +- Fix models stored in eager_load_paths are not picked up by #viable_models ([#3373](https://github.com/railsadminteam/rails_admin/issues/3373), [238f18e](https://github.com/railsadminteam/rails_admin/commit/238f18ee2386f9858670b7995dcb628b8fe6bde9)) +- Fix polymorphic associations don't work with namespaced classes([#3376](https://github.com/railsadminteam/rails_admin/issues/3376)) + +## [2.2.1](https://github.com/railsadminteam/rails_admin/tree/v2.2.0) - 2021-08-08 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.2.0...v2.2.1) + +### Fixed + +- Fix missing select options for single-select enum filters([#3372](https://github.com/railsadminteam/rails_admin/pull/3372)) + +## [2.2.0](https://github.com/railsadminteam/rails_admin/tree/v2.2.0) - 2021-07-24 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.1.1...v2.2.0) + +### Added + +- Support for PaperTrail's alternative versions association name([#3354](https://github.com/railsadminteam/rails_admin/pull/3354)) + +### Changed + +- Update jQuery to 3.x with introducing jQuery.migrate([#3348](https://github.com/railsadminteam/rails_admin/pull/3348), [973dee06](https://github.com/railsadminteam/rails_admin/commit/973dee065938a58d1aef4119a4bc90ac15792421), [#3370](https://github.com/railsadminteam/rails_admin/pull/3370)) +- Update Moment.js to 2.29.1([#3348](https://github.com/railsadminteam/rails_admin/pull/3348), [973dee06](https://github.com/railsadminteam/rails_admin/commit/973dee065938a58d1aef4119a4bc90ac15792421), [7962a194](https://github.com/railsadminteam/rails_admin/commit/7962a19469a709c00f481a50a6d1e7ddd1e37cc6)) +- Update Bootstrap to 3.4.1([#3348](https://github.com/railsadminteam/rails_admin/pull/3348), [973dee06](https://github.com/railsadminteam/rails_admin/commit/973dee065938a58d1aef4119a4bc90ac15792421)) +- Update Bootstrap Datetime Picker to 4.17.49([7962a194](https://github.com/railsadminteam/rails_admin/commit/7962a19469a709c00f481a50a6d1e7ddd1e37cc6)) + +### Removed + +- Remove unnecessary devise patch([#3352](https://github.com/railsadminteam/rails_admin/pull/3352)) + +### Fixed + +- Zeitwerk incompatibility([#3190](https://github.com/railsadminteam/rails_admin/issues/3328), [97ccc289](https://github.com/railsadminteam/rails_admin/commit/97ccc28940d65fee53b30c409c49032fbb0885db)) + +## [2.1.1](https://github.com/railsadminteam/rails_admin/tree/v2.1.1) - 2021-03-14 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.1.0...v2.1.1) + +### Fixed + +- Fix AbstractObject's proxying was incompatible with keyword arguments in Ruby 3.0 ([#3342](https://github.com/railsadminteam/rails_admin/issues/3342)) + +## [2.1.0](https://github.com/railsadminteam/rails_admin/tree/v2.1.0) - 2021-02-28 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.0.2...v2.1.0) + +### Added + +- Ability to set default filter operator for fields ([#3318](https://github.com/railsadminteam/rails_admin/pull/3318)) +- Shrine 3.x support ([#3257](https://github.com/railsadminteam/rails_admin/pull/3257)) +- Rails 6.1 compatibility ([f0c46f1e](https://github.com/railsadminteam/rails_admin/commit/f0c46f1e128b5d31d812ff3a80d15db8692c848b)) + +### Fixed + +- Some translation entries of filtering-multiselect weren't localizable ([#3315](https://github.com/railsadminteam/rails_admin/pull/3315)) +- Thumbnail generation breaks when used with ActiveStorage 6.x and ruby-vips ([#3255](https://github.com/railsadminteam/rails_admin/pull/3255), [2dba791c](https://github.com/railsadminteam/rails_admin/commit/2dba791c9135b3202d662f90fac443d282869bd6)) +- Hide present/blank filter options for required fields ([#3340](https://github.com/railsadminteam/rails_admin/pull/3340)) +- Fix to show correct filename for multiple attachments ([#3295](https://github.com/railsadminteam/rails_admin/pull/3295)) +- Fix encoding detection was incompatible with DB connection proxies like active_record_host_pool gem ([#3313](https://github.com/railsadminteam/rails_admin/pull/3313)) +- Fix hidden fields breaking indentation ([#3278](https://github.com/railsadminteam/rails_admin/pull/3278), [#2487](https://github.com/railsadminteam/rails_admin/issues/2487)) + +### Removed + +- Remove `yell_for_non_accessible_fields` option since it has no effect since 0.5.0 ([#3249](https://github.com/railsadminteam/rails_admin/pull/3249)) + +## [2.0.2](https://github.com/railsadminteam/rails_admin/tree/v2.0.2) - 2020-03-17 + +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.0.1...v2.0.2) + +### Fixed + +- Fix to use I18n to translate the button 'Reset filters'([#3248](https://github.com/railsadminteam/rails_admin/pull/3248)) ### Security -- Fix XSS vulnerability in nested forms([d72090ec](https://github.com/sferik/rails_admin/commit/d72090ec6a07c3b9b7b48ab50f3d405f91ff4375)) +- Fix XSS vulnerability in nested forms([d72090ec](https://github.com/railsadminteam/rails_admin/commit/d72090ec6a07c3b9b7b48ab50f3d405f91ff4375)) -## [2.0.1](https://github.com/sferik/rails_admin/tree/v2.0.1) - 2019-12-31 +## [2.0.1](https://github.com/railsadminteam/rails_admin/tree/v2.0.1) - 2019-12-31 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v2.0.0...v2.0.1) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.0.0...v2.0.1) ### Fixed -- Fix Zeitwerk incompatible behavior of autoloading constants during initialization([#3190](https://github.com/sferik/rails_admin/issues/3190), [e275012b](https://github.com/sferik/rails_admin/commit/e275012b630453cb1187e71a938382a3c5d3ef39)) -- Fix empty fields being hidden regardless of `compact_show_view`([#3213](https://github.com/sferik/rails_admin/pull/3213)) -- Fix `filter_scope` not using `default_search_operator` as default([#3212](https://github.com/sferik/rails_admin/pull/3212)) -- Fix PaperTrail integration returning `nil` as username instead of `whodunnit`([#3210](https://github.com/sferik/rails_admin/pull/3210)) -- Fix Sprockets 4 incompatibility of vendorized Fontawesome([#3204](https://github.com/sferik/rails_admin/issues/3204), [#3207](https://github.com/sferik/rails_admin/pull/3207)) + +- Fix Zeitwerk incompatible behavior of autoloading constants during initialization([#3190](https://github.com/railsadminteam/rails_admin/issues/3190), [e275012b](https://github.com/railsadminteam/rails_admin/commit/e275012b630453cb1187e71a938382a3c5d3ef39)) +- Fix empty fields being hidden regardless of `compact_show_view`([#3213](https://github.com/railsadminteam/rails_admin/pull/3213)) +- Fix `filter_scope` not using `default_search_operator` as default([#3212](https://github.com/railsadminteam/rails_admin/pull/3212)) +- Fix PaperTrail integration returning `nil` as username instead of `whodunnit`([#3210](https://github.com/railsadminteam/rails_admin/pull/3210)) +- Fix Sprockets 4 incompatibility of vendorized Fontawesome([#3204](https://github.com/railsadminteam/rails_admin/issues/3204), [#3207](https://github.com/railsadminteam/rails_admin/pull/3207)) ### Security -- Update moment.js to 2.24.0 to address security vulnerability([#3182](https://github.com/sferik/rails_admin/issues/3182), [#3201](https://github.com/sferik/rails_admin/pull/3201)) +- Update moment.js to 2.24.0 to address security vulnerability([#3182](https://github.com/railsadminteam/rails_admin/issues/3182), [#3201](https://github.com/railsadminteam/rails_admin/pull/3201)) -## [2.0.0](https://github.com/sferik/rails_admin/tree/v2.0.0) - 2019-08-18 +## [2.0.0](https://github.com/railsadminteam/rails_admin/tree/v2.0.0) - 2019-08-18 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v2.0.0.rc...v2.0.0) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.0.0.rc...v2.0.0) ### Fixed -- Fix support for belongs_to with custom primary key was broken in 2.0.0.rc([#3184](https://github.com/sferik/rails_admin/issues/3184), [0e92ca43](https://github.com/sferik/rails_admin/commit/0e92ca43fe7782a8d62ae9285a8d3de7857c9853)) -- Fix missing translation `en.admin.misc.ago`([#3180](https://github.com/sferik/rails_admin/pull/3180)) +- Fix support for belongs_to with custom primary key was broken in 2.0.0.rc([#3184](https://github.com/railsadminteam/rails_admin/issues/3184), [0e92ca43](https://github.com/railsadminteam/rails_admin/commit/0e92ca43fe7782a8d62ae9285a8d3de7857c9853)) +- Fix missing translation `en.admin.misc.ago`([#3180](https://github.com/railsadminteam/rails_admin/pull/3180)) -## [2.0.0.rc](https://github.com/sferik/rails_admin/tree/v2.0.0.rc) - 2019-08-04 +## [2.0.0.rc](https://github.com/railsadminteam/rails_admin/tree/v2.0.0.rc) - 2019-08-04 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v2.0.0.beta...v2.0.0.rc) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v2.0.0.beta...v2.0.0.rc) ### Added -- Add Support for CarrierWave 2.0 multiple file upload's keep, append and reorder feature([fb093e04](https://github.com/sferik/rails_admin/commit/fb093e04502e7bff30594f5baf1227abb7199384)) -- Add ability to configure way how custom actions show up in root/top/sidebar navigation([#2844](https://github.com/sferik/rails_admin/pull/2844)) + +- Add Support for CarrierWave 2.0 multiple file upload's keep, append and reorder feature([fb093e04](https://github.com/railsadminteam/rails_admin/commit/fb093e04502e7bff30594f5baf1227abb7199384)) +- Add ability to configure way how custom actions show up in root/top/sidebar navigation([#2844](https://github.com/railsadminteam/rails_admin/pull/2844)) ### Changed -- [BREAKING CHANGE] Stop authorization adapters assigning attributes on create and update, just check for permission instead([#3120](https://github.com/sferik/rails_admin/pull/3120), [c84d1703](https://github.com/sferik/rails_admin/commit/c84d1703b47b396382d152471dac8fc8dc41aefd)) -- [BREAKING CHANGE] Do not show tableless models by default([#3157](https://github.com/sferik/rails_admin/issues/3157), [87b38b33](https://github.com/sferik/rails_admin/commit/87b38b336cc668a74803dec4628215e2e2941248)) -- [BREAKING CHANGE] Convert empty string into nil for nullable string-like fields to achieve uniqueness-index friendliness([#2099](https://github.com/sferik/rails_admin/issues/2099), [#3172](https://github.com/sferik/rails_admin/issues/3172), [3f9ab1cc](https://github.com/sferik/rails_admin/commit/3f9ab1cc009caa8b466f34da692c3561da2235e4)) -- Extract head from application template for ease of customization([#3114](https://github.com/sferik/rails_admin/pull/3114)) -- Rename `delete_key` to `delete_value`, used to identify which file to delete in multiple file upload([8b8c3a44](https://github.com/sferik/rails_admin/commit/8b8c3a44177465823128e9b48b11467ccf7db001)) -- Get rid of CoffeeScript, use plain JavaScript instead([#3111](https://github.com/sferik/rails_admin/issues/3111), [#3168](https://github.com/sferik/rails_admin/pull/3168)) -- Replace sass-rails with sassc-rails([#3156](https://github.com/sferik/rails_admin/pull/3156)) + +- [BREAKING CHANGE] Stop authorization adapters assigning attributes on create and update, just check for permission instead([#3120](https://github.com/railsadminteam/rails_admin/pull/3120), [c84d1703](https://github.com/railsadminteam/rails_admin/commit/c84d1703b47b396382d152471dac8fc8dc41aefd)) +- [BREAKING CHANGE] Do not show tableless models by default([#3157](https://github.com/railsadminteam/rails_admin/issues/3157), [87b38b33](https://github.com/railsadminteam/rails_admin/commit/87b38b336cc668a74803dec4628215e2e2941248)) +- [BREAKING CHANGE] Convert empty string into nil for nullable string-like fields to achieve uniqueness-index friendliness([#2099](https://github.com/railsadminteam/rails_admin/issues/2099), [#3172](https://github.com/railsadminteam/rails_admin/issues/3172), [3f9ab1cc](https://github.com/railsadminteam/rails_admin/commit/3f9ab1cc009caa8b466f34da692c3561da2235e4)) +- Extract head from application template for ease of customization([#3114](https://github.com/railsadminteam/rails_admin/pull/3114)) +- Rename `delete_key` to `delete_value`, used to identify which file to delete in multiple file upload([8b8c3a44](https://github.com/railsadminteam/rails_admin/commit/8b8c3a44177465823128e9b48b11467ccf7db001)) +- Get rid of CoffeeScript, use plain JavaScript instead([#3111](https://github.com/railsadminteam/rails_admin/issues/3111), [#3168](https://github.com/railsadminteam/rails_admin/pull/3168)) +- Replace sass-rails with sassc-rails([#3156](https://github.com/railsadminteam/rails_admin/pull/3156)) ### Removed -- Drop support for CanCan, please use its successor CanCanCan([6b7495f1](https://github.com/sferik/rails_admin/commit/6b7495f1454e30027a9d77b911206cc7703170a3)) -- Drop support for CanCanCan legacy `can :dashboard` style dashboard ability notation([5bebac24](https://github.com/sferik/rails_admin/commit/5bebac2488906f3739717108efadedaa091ccaf5)) -- Drop Refile support due to maintenance inactivity([25ae06a9](https://github.com/sferik/rails_admin/commit/25ae06a9a6eb534afa4a5f17e64ca346086bb3b8)) + +- Drop support for CanCan, please use its successor CanCanCan([6b7495f1](https://github.com/railsadminteam/rails_admin/commit/6b7495f1454e30027a9d77b911206cc7703170a3)) +- Drop support for CanCanCan legacy `can :dashboard` style dashboard ability notation([5bebac24](https://github.com/railsadminteam/rails_admin/commit/5bebac2488906f3739717108efadedaa091ccaf5)) +- Drop Refile support due to maintenance inactivity([25ae06a9](https://github.com/railsadminteam/rails_admin/commit/25ae06a9a6eb534afa4a5f17e64ca346086bb3b8)) ### Fixed -- Fix PaperTrail pagination breaks when Kaminari's `page_method_name` is set([#3170](https://github.com/sferik/rails_admin/issues/3170), [136b943c](https://github.com/sferik/rails_admin/commit/136b943ce842eba6b0a13dc2956ddc9ce20d006c)) -- Fix failing to pass config location to CKEditor([#3162](https://github.com/sferik/rails_admin/issues/3162), [c38b76d7](https://github.com/sferik/rails_admin/commit/c38b76d707f198be0ac8baa1ff02dde7bd02344f)) -- Fix CarrierWave multiple file uploader breaking when used with Fog([#3070](https://github.com/sferik/rails_admin/issues/3070)) -- Fix placeholder being picked up as a selection in filtering-multiselect([#2807](https://github.com/sferik/rails_admin/issues/2807), [15502601](https://github.com/sferik/rails_admin/commit/15502601ccd0bbbaeb364a0ee605f360d65c5cbb)) -- Fix breaking with has_many and custom primary key([#1878](https://github.com/sferik/rails_admin/issues/1878), [be7d2f4a](https://github.com/sferik/rails_admin/commit/be7d2f4a3ad1fda788f92a0e0dd4a83b98f141f4)) -- Fix to choose right LIKE statement in per-model basis([#1676](https://github.com/sferik/rails_admin/issues/1676), [4ea4575e](https://github.com/sferik/rails_admin/commit/4ea4575e934e26cec4a5b214b9fe68cb96b40247)) -- Fix polymorphic associations not using STI base classes for polymorphic type([#2136](https://github.com/sferik/rails_admin/pull/2136)) + +- Fix PaperTrail pagination breaks when Kaminari's `page_method_name` is set([#3170](https://github.com/railsadminteam/rails_admin/issues/3170), [136b943c](https://github.com/railsadminteam/rails_admin/commit/136b943ce842eba6b0a13dc2956ddc9ce20d006c)) +- Fix failing to pass config location to CKEditor([#3162](https://github.com/railsadminteam/rails_admin/issues/3162), [c38b76d7](https://github.com/railsadminteam/rails_admin/commit/c38b76d707f198be0ac8baa1ff02dde7bd02344f)) +- Fix CarrierWave multiple file uploader breaking when used with Fog([#3070](https://github.com/railsadminteam/rails_admin/issues/3070)) +- Fix placeholder being picked up as a selection in filtering-multiselect([#2807](https://github.com/railsadminteam/rails_admin/issues/2807), [15502601](https://github.com/railsadminteam/rails_admin/commit/15502601ccd0bbbaeb364a0ee605f360d65c5cbb)) +- Fix breaking with has_many and custom primary key([#1878](https://github.com/railsadminteam/rails_admin/issues/1878), [be7d2f4a](https://github.com/railsadminteam/rails_admin/commit/be7d2f4a3ad1fda788f92a0e0dd4a83b98f141f4)) +- Fix to choose right LIKE statement in per-model basis([#1676](https://github.com/railsadminteam/rails_admin/issues/1676), [4ea4575e](https://github.com/railsadminteam/rails_admin/commit/4ea4575e934e26cec4a5b214b9fe68cb96b40247)) +- Fix polymorphic associations not using STI base classes for polymorphic type([#2136](https://github.com/railsadminteam/rails_admin/pull/2136)) ### Security -- Add `rel="noopener"` to all `target="_blank"` links to prevent Reverse tabnabbing([#2960](https://github.com/sferik/rails_admin/issues/2960), [#3169](https://github.com/sferik/rails_admin/pull/3169)) +- Add `rel="noopener"` to all `target="_blank"` links to prevent Reverse tabnabbing([#2960](https://github.com/railsadminteam/rails_admin/issues/2960), [#3169](https://github.com/railsadminteam/rails_admin/pull/3169)) -## [2.0.0.beta](https://github.com/sferik/rails_admin/tree/v2.0.0.beta) - 2019-06-08 +## [2.0.0.beta](https://github.com/railsadminteam/rails_admin/tree/v2.0.0.beta) - 2019-06-08 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v1.4.2...v2.0.0.beta) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.4.2...v2.0.0.beta) ### Added -- Rails 6 support([#3122](https://github.com/sferik/rails_admin/pull/3122)) -- ActionText support([#3144](https://github.com/sferik/rails_admin/issues/3144), [Wiki](https://github.com/sferik/rails_admin/wiki/ActionText)) -- sass-rails 6 support([#3129](https://github.com/sferik/rails_admin/issues/3129)) -- Sidescroll feature([#3017](https://github.com/sferik/rails_admin/pull/3017), [Wiki](https://github.com/sferik/rails_admin/wiki/Horizontally-scrolling-table-with-frozen-columns-in-list-view)) -- Custom search feature([#343](https://github.com/sferik/rails_admin/issues/343), [#3019](https://github.com/sferik/rails_admin/pull/3019), [Wiki](https://github.com/sferik/rails_admin/wiki/Custom-Search)) -- Filtering-select feature for polymorphic association([#2886](https://github.com/sferik/rails_admin/pull/2886)) -- Shrine support([#3081](https://github.com/sferik/rails_admin/pull/3081)) -- Flexibility for localication of *time* ago([#3135](https://github.com/sferik/rails_admin/pull/3135), [49add741](https://github.com/sferik/rails_admin/commit/49add7413794e2a1423b86399ef476414d22970f)) + +- Rails 6 support([#3122](https://github.com/railsadminteam/rails_admin/pull/3122)) +- ActionText support([#3144](https://github.com/railsadminteam/rails_admin/issues/3144), [Wiki](https://github.com/railsadminteam/rails_admin/wiki/ActionText)) +- sass-rails 6 support([#3129](https://github.com/railsadminteam/rails_admin/issues/3129)) +- Sidescroll feature([#3017](https://github.com/railsadminteam/rails_admin/pull/3017), [Wiki](https://github.com/railsadminteam/rails_admin/wiki/Horizontally-scrolling-table-with-frozen-columns-in-list-view)) +- Custom search feature([#343](https://github.com/railsadminteam/rails_admin/issues/343), [#3019](https://github.com/railsadminteam/rails_admin/pull/3019), [Wiki](https://github.com/railsadminteam/rails_admin/wiki/Custom-Search)) +- Filtering-select feature for polymorphic association([#2886](https://github.com/railsadminteam/rails_admin/pull/2886)) +- Shrine support([#3081](https://github.com/railsadminteam/rails_admin/pull/3081)) +- Flexibility for localication of _time_ ago([#3135](https://github.com/railsadminteam/rails_admin/pull/3135), [49add741](https://github.com/railsadminteam/rails_admin/commit/49add7413794e2a1423b86399ef476414d22970f)) ### Changed -- Vendorize font-awesome to allow using different version in app([#3039](https://github.com/sferik/rails_admin/issues/3039)) -- Stop inlining JavaScripts for CSP friendliness([#3087](https://github.com/sferik/rails_admin/issues/3087)) -- Richtext editors now uses CDN-hosted assets([#3126](https://github.com/sferik/rails_admin/issues/3126)) + +- Vendorize font-awesome to allow using different version in app([#3039](https://github.com/railsadminteam/rails_admin/issues/3039)) +- Stop inlining JavaScripts for CSP friendliness([#3087](https://github.com/railsadminteam/rails_admin/issues/3087)) +- Richtext editors now uses CDN-hosted assets([#3126](https://github.com/railsadminteam/rails_admin/issues/3126)) ### Removed -- Remove deprecated DSL syntax for richtext editors([e0b390d9](https://github.com/sferik/rails_admin/commit/e0b390d99eab64c99f1f3cccae2029649e90e11c)) -- Drop support for Ruby 2.1 and Rails 4.x([dd247804](https://github.com/sferik/rails_admin/commit/dd24780445f4dd676ae033c69a5b64347b80c3bc)) + +- Remove deprecated DSL syntax for richtext editors([e0b390d9](https://github.com/railsadminteam/rails_admin/commit/e0b390d99eab64c99f1f3cccae2029649e90e11c)) +- Drop support for Ruby 2.1 and Rails 4.x([dd247804](https://github.com/railsadminteam/rails_admin/commit/dd24780445f4dd676ae033c69a5b64347b80c3bc)) ### Fixed -- Fix Mongoid query and filter parsing value twice([#2755](https://github.com/sferik/rails_admin/issues/2755)) -- Fix thread-safety issues([#2897](https://github.com/sferik/rails_admin/issues/2897), [#2942](https://github.com/sferik/rails_admin/issues/2942), [1d22bc66](https://github.com/sferik/rails_admin/commit/1d22bc66168ac9ea478ea95b4b3b79f41263c0bd)) -- Fix compact_show_view not showing Boolean falses([#2416](https://github.com/sferik/rails_admin/issues/2416)) -- Fix PaperTrail fail to fetch versions for STI subclasses([#2865](https://github.com/sferik/rails_admin/pull/2865)) -- Fix Dragonfly factory breaks if a model not extending Dragonfly::Model is passed([#2720](https://github.com/sferik/rails_admin/pull/2720)) -- Fix PaperTrail adapter not using Kaminari's `page_method_name` for pagination([#2712](https://github.com/sferik/rails_admin/pull/2712)) -- Fix #bulk_menu was not using passed `abstract_model` ([#2782](https://github.com/sferik/rails_admin/pull/2782)) -- Fix wrong styles when using multiple instances of CodeMirror([#3107](https://github.com/sferik/rails_admin/pull/3107)) -- Fix password being cleared when used with Devise 4.6([72bc0373](https://github.com/sferik/rails_admin/commit/72bc03736162ffef8e5b99f42ca605d17fe7e7d0)) -- ActiveStorage factory caused const missing for Mongoid([#3088](https://github.com/sferik/rails_admin/pull/3088), [db927687](https://github.com/sferik/rails_admin/commit/db9276879c8e8c5e8772261725ef0e0cdadd9cf1)) -- Fix exact matches were using LIKE, which was not index-friendly([#3000](https://github.com/sferik/rails_admin/pull/3000)) -- Middleware check failed when using RedisStore([#3076](https://github.com/sferik/rails_admin/issues/3076)) -- Fix field being reset to default after an error([#3066](https://github.com/sferik/rails_admin/pull/3066)) +- Fix Mongoid query and filter parsing value twice([#2755](https://github.com/railsadminteam/rails_admin/issues/2755)) +- Fix thread-safety issues([#2897](https://github.com/railsadminteam/rails_admin/issues/2897), [#2942](https://github.com/railsadminteam/rails_admin/issues/2942), [1d22bc66](https://github.com/railsadminteam/rails_admin/commit/1d22bc66168ac9ea478ea95b4b3b79f41263c0bd)) +- Fix compact_show_view not showing Boolean falses([#2416](https://github.com/railsadminteam/rails_admin/issues/2416)) +- Fix PaperTrail fail to fetch versions for STI subclasses([#2865](https://github.com/railsadminteam/rails_admin/pull/2865)) +- Fix Dragonfly factory breaks if a model not extending Dragonfly::Model is passed([#2720](https://github.com/railsadminteam/rails_admin/pull/2720)) +- Fix PaperTrail adapter not using Kaminari's `page_method_name` for pagination([#2712](https://github.com/railsadminteam/rails_admin/pull/2712)) +- Fix #bulk_menu was not using passed `abstract_model` ([#2782](https://github.com/railsadminteam/rails_admin/pull/2782)) +- Fix wrong styles when using multiple instances of CodeMirror([#3107](https://github.com/railsadminteam/rails_admin/pull/3107)) +- Fix password being cleared when used with Devise 4.6([72bc0373](https://github.com/railsadminteam/rails_admin/commit/72bc03736162ffef8e5b99f42ca605d17fe7e7d0)) +- ActiveStorage factory caused const missing for Mongoid([#3088](https://github.com/railsadminteam/rails_admin/pull/3088), [db927687](https://github.com/railsadminteam/rails_admin/commit/db9276879c8e8c5e8772261725ef0e0cdadd9cf1)) +- Fix exact matches were using LIKE, which was not index-friendly([#3000](https://github.com/railsadminteam/rails_admin/pull/3000)) +- Middleware check failed when using RedisStore([#3076](https://github.com/railsadminteam/rails_admin/issues/3076)) +- Fix field being reset to default after an error([#3066](https://github.com/railsadminteam/rails_admin/pull/3066)) -## [1.4.2](https://github.com/sferik/rails_admin/tree/v1.4.2) - 2018-09-23 +## [1.4.2](https://github.com/railsadminteam/rails_admin/tree/v1.4.2) - 2018-09-23 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v1.4.1...v1.4.2) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.4.1...v1.4.2) ### Fixed -- Fix `can't modify frozen Array` error on startup([#3060](https://github.com/sferik/rails_admin/issues/3060)) -- Fix deprecation warning with PaperTrail.whodunnit([#3059](https://github.com/sferik/rails_admin/pull/3059)) +- Fix `can't modify frozen Array` error on startup([#3060](https://github.com/railsadminteam/rails_admin/issues/3060)) +- Fix deprecation warning with PaperTrail.whodunnit([#3059](https://github.com/railsadminteam/rails_admin/pull/3059)) -## [1.4.1](https://github.com/sferik/rails_admin/tree/v1.4.1) - 2018-08-19 +## [1.4.1](https://github.com/railsadminteam/rails_admin/tree/v1.4.1) - 2018-08-19 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v1.4.0...v1.4.1) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.4.0...v1.4.1) ### Fixed -- Export crashes for models with JSON field([#3056](https://github.com/sferik/rails_admin/pull/3056)) -- Middlewares being mangled by engine initializer, causing app's session store configuration to be overwritten([#3048](https://github.com/sferik/rails_admin/issues/3048), [59478af9](https://github.com/sferik/rails_admin/commit/59478af9a05c76bdfe35e94e63c60ba89c27a483)) +- Export crashes for models with JSON field([#3056](https://github.com/railsadminteam/rails_admin/pull/3056)) +- Middlewares being mangled by engine initializer, causing app's session store configuration to be overwritten([#3048](https://github.com/railsadminteam/rails_admin/issues/3048), [59478af9](https://github.com/railsadminteam/rails_admin/commit/59478af9a05c76bdfe35e94e63c60ba89c27a483)) -## [1.4.0](https://github.com/sferik/rails_admin/tree/v1.4.0) - 2018-07-22 +## [1.4.0](https://github.com/railsadminteam/rails_admin/tree/v1.4.0) - 2018-07-22 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v1.3.0...v1.4.0) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.3.0...v1.4.0) ### Added -- Support for ActiveStorage([#2990](https://github.com/sferik/rails_admin/issues/2990), [#3037](https://github.com/sferik/rails_admin/pull/3037)) -- Support for multiple file upload for ActiveStorage and CarrierWave ([5bb2d375](https://github.com/sferik/rails_admin/commit/5bb2d375a236268e51c7e8682c2d110d9e52970f)) -- Support for Mongoid 7.0([9ef623f6](https://github.com/sferik/rails_admin/commit/9ef623f6cba73adbf86833d9eb07f1be3924a133), [#3013](https://github.com/sferik/rails_admin/issues/3013)) -- Support for CanCanCan 2.0([a32d49e4](https://github.com/sferik/rails_admin/commit/a32d49e4b96944905443588a1216b3362ee64c1a), [#2901](https://github.com/sferik/rails_admin/issues/2901)) -- Support for Pundit 2.0([bc60c978](https://github.com/sferik/rails_admin/commit/bc60c978adfebe09cdad2c199878d8ff966374f1)) -- Support for jquery-ui-rails 6.0([#2951](https://github.com/sferik/rails_admin/issues/2951), [#3003](https://github.com/sferik/rails_admin/issues/3003)) + +- Support for ActiveStorage([#2990](https://github.com/railsadminteam/rails_admin/issues/2990), [#3037](https://github.com/railsadminteam/rails_admin/pull/3037)) +- Support for multiple file upload for ActiveStorage and CarrierWave ([5bb2d375](https://github.com/railsadminteam/rails_admin/commit/5bb2d375a236268e51c7e8682c2d110d9e52970f)) +- Support for Mongoid 7.0([9ef623f6](https://github.com/railsadminteam/rails_admin/commit/9ef623f6cba73adbf86833d9eb07f1be3924a133), [#3013](https://github.com/railsadminteam/rails_admin/issues/3013)) +- Support for CanCanCan 2.0([a32d49e4](https://github.com/railsadminteam/rails_admin/commit/a32d49e4b96944905443588a1216b3362ee64c1a), [#2901](https://github.com/railsadminteam/rails_admin/issues/2901)) +- Support for Pundit 2.0([bc60c978](https://github.com/railsadminteam/rails_admin/commit/bc60c978adfebe09cdad2c199878d8ff966374f1)) +- Support for jquery-ui-rails 6.0([#2951](https://github.com/railsadminteam/rails_admin/issues/2951), [#3003](https://github.com/railsadminteam/rails_admin/issues/3003)) ### Fixed -- Make code reloading work([#3041](https://github.com/sferik/rails_admin/pull/3041)) -- Improved support for Rails API mode, requiring needed middlewares in engine's initializer([#2919](https://github.com/sferik/rails_admin/issues/2919), [#3006](https://github.com/sferik/rails_admin/pull/3006)) -- Make the link text to uploaded file shorter, instead of showing full url([#2983](https://github.com/sferik/rails_admin/pull/2983)) -- Fix duplication of filters on browser back([#2998](https://github.com/sferik/rails_admin/pull/2998)) -- Fix "can't modify frozen array" exception on code reload([#2999](https://github.com/sferik/rails_admin/pull/2999)) -- Fix incorrectly comparing numeric columns with empty string when handling blank operator([#3007](https://github.com/sferik/rails_admin/pull/3007)) +- Make code reloading work([#3041](https://github.com/railsadminteam/rails_admin/pull/3041)) +- Improved support for Rails API mode, requiring needed middlewares in engine's initializer([#2919](https://github.com/railsadminteam/rails_admin/issues/2919), [#3006](https://github.com/railsadminteam/rails_admin/pull/3006)) +- Make the link text to uploaded file shorter, instead of showing full url([#2983](https://github.com/railsadminteam/rails_admin/pull/2983)) +- Fix duplication of filters on browser back([#2998](https://github.com/railsadminteam/rails_admin/pull/2998)) +- Fix "can't modify frozen array" exception on code reload([#2999](https://github.com/railsadminteam/rails_admin/pull/2999)) +- Fix incorrectly comparing numeric columns with empty string when handling blank operator([#3007](https://github.com/railsadminteam/rails_admin/pull/3007)) -## [1.3.0](https://github.com/sferik/rails_admin/tree/v1.3.0) - 2018-02-18 +## [1.3.0](https://github.com/railsadminteam/rails_admin/tree/v1.3.0) - 2018-02-18 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v1.2.0...v1.3.0) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.2.0...v1.3.0) ### Added -- Configurability for forgery protection setting([#2989](https://github.com/sferik/rails_admin/pull/2989)) -- Configurability for the number of audit records displayed into dashboard([#2982](https://github.com/sferik/rails_admin/pull/2982)) -- Add limited pagination mode, which doesn't require count query([#2968](https://github.com/sferik/rails_admin/pull/2968)) -- Prettier output of JSON field value([#2937](https://github.com/sferik/rails_admin/pull/2937), [#2973](https://github.com/sferik/rails_admin/pull/2973), [#2980](https://github.com/sferik/rails_admin/pull/2980)) -- Add markdown field support through SimpleMDE([#2949](https://github.com/sferik/rails_admin/pull/2949)) -- Checkboxes for bulk actions in index page can be turned off now([#2917](https://github.com/sferik/rails_admin/pull/2917)) + +- Configurability for forgery protection setting([#2989](https://github.com/railsadminteam/rails_admin/pull/2989)) +- Configurability for the number of audit records displayed into dashboard([#2982](https://github.com/railsadminteam/rails_admin/pull/2982)) +- Add limited pagination mode, which doesn't require count query([#2968](https://github.com/railsadminteam/rails_admin/pull/2968)) +- Prettier output of JSON field value([#2937](https://github.com/railsadminteam/rails_admin/pull/2937), [#2973](https://github.com/railsadminteam/rails_admin/pull/2973), [#2980](https://github.com/railsadminteam/rails_admin/pull/2980)) +- Add markdown field support through SimpleMDE([#2949](https://github.com/railsadminteam/rails_admin/pull/2949)) +- Checkboxes for bulk actions in index page can be turned off now([#2917](https://github.com/railsadminteam/rails_admin/pull/2917)) ### Fixed -- Parse JS translations as JSON([#2925](https://github.com/sferik/rails_admin/pull/2925)) -- Re-selecting an item after unselecting has no effect in filtering-multiselect([#2912](https://github.com/sferik/rails_admin/issues/2912)) -- Stop memoization of datetime parser to handle locale changes([#2824](https://github.com/sferik/rails_admin/pull/2824)) -- Filters for ActiveRecord Enum field behaved incorrectly for enums whose labels are different from values([#2971](https://github.com/sferik/rails_admin/pull/2971)) -- Client-side required validation was not enforced in filtering-select widget([#2905](https://github.com/sferik/rails_admin/pull/2905)) -- Filter refresh button was broken([#2890](https://github.com/sferik/rails_admin/pull/2890)) + +- Parse JS translations as JSON([#2925](https://github.com/railsadminteam/rails_admin/pull/2925)) +- Re-selecting an item after unselecting has no effect in filtering-multiselect([#2912](https://github.com/railsadminteam/rails_admin/issues/2912)) +- Stop memoization of datetime parser to handle locale changes([#2824](https://github.com/railsadminteam/rails_admin/pull/2824)) +- Filters for ActiveRecord Enum field behaved incorrectly for enums whose labels are different from values([#2971](https://github.com/railsadminteam/rails_admin/pull/2971)) +- Client-side required validation was not enforced in filtering-select widget([#2905](https://github.com/railsadminteam/rails_admin/pull/2905)) +- Filter refresh button was broken([#2890](https://github.com/railsadminteam/rails_admin/pull/2890)) ### Security -- Fix XSS vulnerability in filter and multi-select widget([#2985](https://github.com/sferik/rails_admin/issues/2985), [44f09ed7](https://github.com/sferik/rails_admin/commit/44f09ed72b5e0e917a5d61bd89c48d97c494b41c)) +- Fix XSS vulnerability in filter and multi-select widget([#2985](https://github.com/railsadminteam/rails_admin/issues/2985), [44f09ed7](https://github.com/railsadminteam/rails_admin/commit/44f09ed72b5e0e917a5d61bd89c48d97c494b41c)) -## [1.2.0](https://github.com/sferik/rails_admin/tree/v1.2.0) - 2017-05-31 +## [1.2.0](https://github.com/railsadminteam/rails_admin/tree/v1.2.0) - 2017-05-31 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v1.1.1...v1.2.0) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.1.1...v1.2.0) ### Added -- Add ILIKE support for PostgreSQL/PostGIS adapter, multibyte downcase for other adapters([#2766](https://github.com/sferik/rails_admin/pull/2766)) -- Support for UUID query([#2766](https://github.com/sferik/rails_admin/pull/2766)) -- Support for Haml 5([#2840](https://github.com/sferik/rails_admin/pull/2840), [#2870](https://github.com/sferik/rails_admin/pull/2870), [#2877](https://github.com/sferik/rails_admin/pull/2877)) -- Add instance option to append a CSS class for rows([#2860](https://github.com/sferik/rails_admin/pull/2860)) + +- Add ILIKE support for PostgreSQL/PostGIS adapter, multibyte downcase for other adapters([#2766](https://github.com/railsadminteam/rails_admin/pull/2766)) +- Support for UUID query([#2766](https://github.com/railsadminteam/rails_admin/pull/2766)) +- Support for Haml 5([#2840](https://github.com/railsadminteam/rails_admin/pull/2840), [#2870](https://github.com/railsadminteam/rails_admin/pull/2870), [#2877](https://github.com/railsadminteam/rails_admin/pull/2877)) +- Add instance option to append a CSS class for rows([#2860](https://github.com/railsadminteam/rails_admin/pull/2860)) ### Fixed -- Remove usage of alias_method_chain, deprecated in Rails 5.0([#2864](https://github.com/sferik/rails_admin/pull/2864)) -- Load models from eager_load, not autoload_paths([#2771](https://github.com/sferik/rails_admin/pull/2771)) -- jQuery 3.0 doesn't have size(), use length instead([#2841](https://github.com/sferik/rails_admin/pull/2841)) -- Prepopulation of the new form didn't work with namespaced models([#2701](https://github.com/sferik/rails_admin/pull/2701)) +- Remove usage of alias_method_chain, deprecated in Rails 5.0([#2864](https://github.com/railsadminteam/rails_admin/pull/2864)) +- Load models from eager_load, not autoload_paths([#2771](https://github.com/railsadminteam/rails_admin/pull/2771)) +- jQuery 3.0 doesn't have size(), use length instead([#2841](https://github.com/railsadminteam/rails_admin/pull/2841)) +- Prepopulation of the new form didn't work with namespaced models([#2701](https://github.com/railsadminteam/rails_admin/pull/2701)) -## [1.1.1](https://github.com/sferik/rails_admin/tree/v1.1.1) - 2016-12-25 +## [1.1.1](https://github.com/railsadminteam/rails_admin/tree/v1.1.1) - 2016-12-25 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v1.1.0...v1.1.1) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.1.0...v1.1.1) ### Fixed -- CSV export broke with empty tables([#2796](https://github.com/sferik/rails_admin/issues/2796), [#2797](https://github.com/sferik/rails_admin/pull/2797)) -- ActiveRecord adapter's #encoding did not work with Oracle enhanced adapter([#2789](https://github.com/sferik/rails_admin/pull/2789)) -- ActiveRecord 5 belongs_to presence validators were unintentionally disabled due to initialization mishandling([#2785](https://github.com/sferik/rails_admin/issues/2785), [#2786](https://github.com/sferik/rails_admin/issues/2786)) -- Destroy failure caused subsequent index action to return 404, instead of 200([#2775](https://github.com/sferik/rails_admin/issues/2775), [#2776](https://github.com/sferik/rails_admin/pull/2776)) -- CSVConverter#to_csv now accepts string-keyed hashes([#2740](https://github.com/sferik/rails_admin/issues/2740), [#2741](https://github.com/sferik/rails_admin/pull/2741)) + +- CSV export broke with empty tables([#2796](https://github.com/railsadminteam/rails_admin/issues/2796), [#2797](https://github.com/railsadminteam/rails_admin/pull/2797)) +- ActiveRecord adapter's #encoding did not work with Oracle enhanced adapter([#2789](https://github.com/railsadminteam/rails_admin/pull/2789)) +- ActiveRecord 5 belongs_to presence validators were unintentionally disabled due to initialization mishandling([#2785](https://github.com/railsadminteam/rails_admin/issues/2785), [#2786](https://github.com/railsadminteam/rails_admin/issues/2786)) +- Destroy failure caused subsequent index action to return 404, instead of 200([#2775](https://github.com/railsadminteam/rails_admin/issues/2775), [#2776](https://github.com/railsadminteam/rails_admin/pull/2776)) +- CSVConverter#to_csv now accepts string-keyed hashes([#2740](https://github.com/railsadminteam/rails_admin/issues/2740), [#2741](https://github.com/railsadminteam/rails_admin/pull/2741)) ### Security -- Fix CSRF vulnerability([b13e879e](https://github.com/sferik/rails_admin/commit/b13e879eb93b661204e9fb5e55f7afa4f397537a)) +- Fix CSRF vulnerability([b13e879e](https://github.com/railsadminteam/rails_admin/commit/b13e879eb93b661204e9fb5e55f7afa4f397537a)) -## [1.1.0](https://github.com/sferik/rails_admin/tree/v1.1.0) - 2016-10-30 +## [1.1.0](https://github.com/railsadminteam/rails_admin/tree/v1.1.0) - 2016-10-30 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v1.0.0...v1.1.0) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.0.0...v1.1.0) ### Added -- DSL for association eager-loading([#1325](https://github.com/sferik/rails_admin/issues/1325), [#1342](https://github.com/sferik/rails_admin/issues/1342)) + +- DSL for association eager-loading([#1325](https://github.com/railsadminteam/rails_admin/issues/1325), [#1342](https://github.com/railsadminteam/rails_admin/issues/1342)) ### Fixed -- Fix nested has_many form failing to add items([#2737](https://github.com/sferik/rails_admin/pull/2737)) +- Fix nested has_many form failing to add items([#2737](https://github.com/railsadminteam/rails_admin/pull/2737)) -## [1.0.0](https://github.com/sferik/rails_admin/tree/v1.0.0) - 2016-09-19 +## [1.0.0](https://github.com/railsadminteam/rails_admin/tree/v1.0.0) - 2016-09-19 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v1.0.0.rc...v1.0.0) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v1.0.0.rc...v1.0.0) ### Added -- Introduce setup hook for authorization/auditing adapters([ba2088c6](https://github.com/sferik/rails_admin/commit/ba2088c6ecabd354b4b67c50bb00fccdbd1e6240)) -- Add viewport meta tag for mobile layout adjustment([#2664](https://github.com/sferik/rails_admin/pull/2664)) -- Support for ActiveRecord::Enum using string columns([#2680](https://github.com/sferik/rails_admin/pull/2680)) + +- Introduce setup hook for authorization/auditing adapters([ba2088c6](https://github.com/railsadminteam/rails_admin/commit/ba2088c6ecabd354b4b67c50bb00fccdbd1e6240)) +- Add viewport meta tag for mobile layout adjustment([#2664](https://github.com/railsadminteam/rails_admin/pull/2664)) +- Support for ActiveRecord::Enum using string columns([#2680](https://github.com/railsadminteam/rails_admin/pull/2680)) ### Changed -- Limit children for deletion notice([#2491](https://github.com/sferik/rails_admin/pull/2491)) -- [BREAKING CHANGE] Change parent controller to ActionController::Base for out-of-box support of Rails 5 API mode([#2688](https://github.com/sferik/rails_admin/issues/2688)) + +- Limit children for deletion notice([#2491](https://github.com/railsadminteam/rails_admin/pull/2491)) +- [BREAKING CHANGE] Change parent controller to ActionController::Base for out-of-box support of Rails 5 API mode([#2688](https://github.com/railsadminteam/rails_admin/issues/2688)) - To keep old behavior, add `config.parent_controller = '::ApplicationController'` in your RailsAdmin initializer. ### Fixed -- ActiveRecord Enum fields could not be updated correctly([#2659](https://github.com/sferik/rails_admin/pull/2659), [#2713](https://github.com/sferik/rails_admin/issues/2713)) -- Fix performance issue with filtering-multiselect widget([#2715](https://github.com/sferik/rails_admin/pull/2715)) -- Restore back rails_admin_controller?([#2268](https://github.com/sferik/rails_admin/issues/2268)) -- Duplication of autocomplete fields when using browser back/forward buttons([#2677](https://github.com/sferik/rails_admin/issues/2677), [#2678](https://github.com/sferik/rails_admin/pull/2678)) -- Filter refresh button was broken([#2705](https://github.com/sferik/rails_admin/issues/2705), [#2706](https://github.com/sferik/rails_admin/pull/2706)) -- Fix presence filtering on boolean columns([#1099](https://github.com/sferik/rails_admin/issues/1099), [#2675](https://github.com/sferik/rails_admin/pull/2675)) -- Pundit::AuthorizationNotPerformedError was raised when used with Pundit([#2683](https://github.com/sferik/rails_admin/pull/2683)) +- ActiveRecord Enum fields could not be updated correctly([#2659](https://github.com/railsadminteam/rails_admin/pull/2659), [#2713](https://github.com/railsadminteam/rails_admin/issues/2713)) +- Fix performance issue with filtering-multiselect widget([#2715](https://github.com/railsadminteam/rails_admin/pull/2715)) +- Restore back rails_admin_controller?([#2268](https://github.com/railsadminteam/rails_admin/issues/2268)) +- Duplication of autocomplete fields when using browser back/forward buttons([#2677](https://github.com/railsadminteam/rails_admin/issues/2677), [#2678](https://github.com/railsadminteam/rails_admin/pull/2678)) +- Filter refresh button was broken([#2705](https://github.com/railsadminteam/rails_admin/issues/2705), [#2706](https://github.com/railsadminteam/rails_admin/pull/2706)) +- Fix presence filtering on boolean columns([#1099](https://github.com/railsadminteam/rails_admin/issues/1099), [#2675](https://github.com/railsadminteam/rails_admin/pull/2675)) +- Pundit::AuthorizationNotPerformedError was raised when used with Pundit([#2683](https://github.com/railsadminteam/rails_admin/pull/2683)) -## [1.0.0.rc](https://github.com/sferik/rails_admin/tree/v1.0.0.rc) - 2016-07-18 +## [1.0.0.rc](https://github.com/railsadminteam/rails_admin/tree/v1.0.0.rc) - 2016-07-18 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v0.8.1...v1.0.0.rc) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v0.8.1...v1.0.0.rc) ### Added + - Rails 5 support -- PaperTrail 5 support([9c42783a](https://github.com/sferik/rails_admin/commit/9c42783aa65b704f4a5d467608c49b746c47b81b)) -- Support for multiple configuration blocks([#1781](https://github.com/sferik/rails_admin/pull/1781), [#2670](https://github.com/sferik/rails_admin/pull/2670)) -- Default association limit is now configurable([#2508](https://github.com/sferik/rails_admin/pull/2058)) +- PaperTrail 5 support([9c42783a](https://github.com/railsadminteam/rails_admin/commit/9c42783aa65b704f4a5d467608c49b746c47b81b)) +- Support for multiple configuration blocks([#1781](https://github.com/railsadminteam/rails_admin/pull/1781), [#2670](https://github.com/railsadminteam/rails_admin/pull/2670)) +- Default association limit is now configurable([#2508](https://github.com/railsadminteam/rails_admin/pull/2058)) ### Changed -- Prefix kaminari bootstrap views with `ra-` to avoid name conflict([#2283](https://github.com/sferik/rails_admin/issues/2283), [#2651](https://github.com/sferik/rails_admin/pull/2651)) -- Gravatar icon is now optional([#2570](https://github.com/sferik/rails_admin/pull/2570)) -- Improve bootstrap-wysihtml5-rails support([#2650](https://github.com/sferik/rails_admin/pull/2650)) -- Explicitly call the #t method on I18n([#2564](https://github.com/sferik/rails_admin/pull/2564)) -- Improve dashboard performance by querying with id instead of updated_at([#2514](https://github.com/sferik/rails_admin/issues/2514), [#2551](https://github.com/sferik/rails_admin/pull/2551)) -- Improve encoding support in CSV converter([#2508](https://github.com/sferik/rails_admin/pull/2508), [dca8911f](https://github.com/sferik/rails_admin/commit/dca8911f240ea11ebb186c33573188aa9e1b031d)) -- Add SVG file extension to the image detection method([#2533](https://github.com/sferik/rails_admin/pull/2533)) -- Update linear gradient syntax to make autoprefixer happy([#2531](https://github.com/sferik/rails_admin/pull/2531)) -- Improve export layout ([#2505](https://github.com/sferik/rails_admin/pull/2505)) + +- Prefix kaminari bootstrap views with `ra-` to avoid name conflict([#2283](https://github.com/railsadminteam/rails_admin/issues/2283), [#2651](https://github.com/railsadminteam/rails_admin/pull/2651)) +- Gravatar icon is now optional([#2570](https://github.com/railsadminteam/rails_admin/pull/2570)) +- Improve bootstrap-wysihtml5-rails support([#2650](https://github.com/railsadminteam/rails_admin/pull/2650)) +- Explicitly call the #t method on I18n([#2564](https://github.com/railsadminteam/rails_admin/pull/2564)) +- Improve dashboard performance by querying with id instead of updated_at([#2514](https://github.com/railsadminteam/rails_admin/issues/2514), [#2551](https://github.com/railsadminteam/rails_admin/pull/2551)) +- Improve encoding support in CSV converter([#2508](https://github.com/railsadminteam/rails_admin/pull/2508), [dca8911f](https://github.com/railsadminteam/rails_admin/commit/dca8911f240ea11ebb186c33573188aa9e1b031d)) +- Add SVG file extension to the image detection method([#2533](https://github.com/railsadminteam/rails_admin/pull/2533)) +- Update linear gradient syntax to make autoprefixer happy([#2531](https://github.com/railsadminteam/rails_admin/pull/2531)) +- Improve export layout ([#2505](https://github.com/railsadminteam/rails_admin/pull/2505)) ### Removed -- Remove safe_yaml dependency([#2397](https://github.com/sferik/rails_admin/pull/2397)) + +- Remove safe_yaml dependency([#2397](https://github.com/railsadminteam/rails_admin/pull/2397)) - Drop support for Ruby < 2.1.0 ### Fixed -- Pagination did not work when showing all history([#2553](https://github.com/sferik/rails_admin/pull/2553)) -- Make filter-box label clickable([#2573](https://github.com/sferik/rails_admin/pull/2573)) -- Colorpicker form did not have the default css class `form-control`([#2571](https://github.com/sferik/rails_admin/pull/2571)) -- Stop assuming locale en is available([#2155](https://github.com/sferik/rails_admin/issues/2155)) -- Fix undefined method error with nested polymorphics([#1338](https://github.com/sferik/rails_admin/issues/1338), [#2110](https://github.com/sferik/rails_admin/pull/2110)) -- Fix issue with nav does not check pjax config from an action([#2309](https://github.com/sferik/rails_admin/pull/2309)) -- Model label should be pluralized with locale([#1983](https://github.com/sferik/rails_admin/pull/1983)) -- Fix delocalize strftime_format for DateTime.strptime to support minus([#2547](https://github.com/sferik/rails_admin/pull/2547)) -- Fix Syntax Error in removal of new nested entity([#2539](https://github.com/sferik/rails_admin/pull/2539)) -- Fix momentjs translations for '%-d' format day of the month([#2540](https://github.com/sferik/rails_admin/pull/2540)) -- Fix Mongoid BSON object field ([#2495](https://github.com/sferik/rails_admin/issues/2495)) -- Make browser ignore validaitons of removed nested child models([#2443](https://github.com/sferik/rails_admin/issues/2443), [#2490](https://github.com/sferik/rails_admin/pull/2490)) +- Pagination did not work when showing all history([#2553](https://github.com/railsadminteam/rails_admin/pull/2553)) +- Make filter-box label clickable([#2573](https://github.com/railsadminteam/rails_admin/pull/2573)) +- Colorpicker form did not have the default css class `form-control`([#2571](https://github.com/railsadminteam/rails_admin/pull/2571)) +- Stop assuming locale en is available([#2155](https://github.com/railsadminteam/rails_admin/issues/2155)) +- Fix undefined method error with nested polymorphics([#1338](https://github.com/railsadminteam/rails_admin/issues/1338), [#2110](https://github.com/railsadminteam/rails_admin/pull/2110)) +- Fix issue with nav does not check pjax config from an action([#2309](https://github.com/railsadminteam/rails_admin/pull/2309)) +- Model label should be pluralized with locale([#1983](https://github.com/railsadminteam/rails_admin/pull/1983)) +- Fix delocalize strftime_format for DateTime.strptime to support minus([#2547](https://github.com/railsadminteam/rails_admin/pull/2547)) +- Fix Syntax Error in removal of new nested entity([#2539](https://github.com/railsadminteam/rails_admin/pull/2539)) +- Fix momentjs translations for '%-d' format day of the month([#2540](https://github.com/railsadminteam/rails_admin/pull/2540)) +- Fix Mongoid BSON object field ([#2495](https://github.com/railsadminteam/rails_admin/issues/2495)) +- Make browser ignore validations of removed nested child models([#2443](https://github.com/railsadminteam/rails_admin/issues/2443), [#2490](https://github.com/railsadminteam/rails_admin/pull/2490)) -## [0.8.1](https://github.com/sferik/rails_admin/tree/v0.8.1) - 2015-11-24 +## [0.8.1](https://github.com/railsadminteam/rails_admin/tree/v0.8.1) - 2015-11-24 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v0.8.0...v0.8.1) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v0.8.0...v0.8.1) ### Fixed -- `vendor/` directory was missing from gemspec([#2481](https://github.com/sferik/rails_admin/issues/2481), [#2482](https://github.com/sferik/rails_admin/pull/2482)) +- `vendor/` directory was missing from gemspec([#2481](https://github.com/railsadminteam/rails_admin/issues/2481), [#2482](https://github.com/railsadminteam/rails_admin/pull/2482)) -## [0.8.0](https://github.com/sferik/rails_admin/tree/v0.8.0) - 2015-11-23 +## [0.8.0](https://github.com/railsadminteam/rails_admin/tree/v0.8.0) - 2015-11-23 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v0.7.0...v0.8.0) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v0.7.0...v0.8.0) ### Added -- Feature to deactivate filtering-multiselect widget's remove buttons through `removable?` field option([#2446](https://github.com/sferik/rails_admin/issues/2446)) -- Pundit integration([#2399](https://github.com/sferik/rails_admin/pull/2399) by Team CodeBenders, RGSoC'15) -- Refile support([#2385](https://github.com/sferik/rails_admin/pull/2385)) + +- Feature to deactivate filtering-multiselect widget's remove buttons through `removable?` field option([#2446](https://github.com/railsadminteam/rails_admin/issues/2446)) +- Pundit integration([#2399](https://github.com/railsadminteam/rails_admin/pull/2399) by Team CodeBenders, RGSoC'15) +- Refile support([#2385](https://github.com/railsadminteam/rails_admin/pull/2385)) ### Changed -- Some UI improvements in export view([#2394](https://github.com/sferik/rails_admin/pull/2394)) -- `rails_admin/custom/variables.scss` is now imported first to take advantage of Sass's `default!`([#2404](https://github.com/sferik/rails_admin/pull/2404)) -- Proxy classes now inherit from BasicObject([#2434](https://github.com/sferik/rails_admin/issues/2434)) -- Show sidebar scrollbar only on demand([#2419](https://github.com/sferik/rails_admin/pull/2419)) -- RailsAdmin no longer gets excluded from NewRelic instrumentation by default([#2402](https://github.com/sferik/rails_admin/pull/2402)) -- Improve efficiency of filter query in Postgres([#2401](https://github.com/sferik/rails_admin/pull/2401)) -- Replace old jQueryUI datepicker with jQuery Bootstrap datetimepicker ([#2391](https://github.com/sferik/rails_admin/pull/2391)) -- Turn Hash#symbolize into a helper to prevent namespace conflict([#2388](https://github.com/sferik/rails_admin/pull/2388)) + +- Some UI improvements in export view([#2394](https://github.com/railsadminteam/rails_admin/pull/2394)) +- `rails_admin/custom/variables.scss` is now imported first to take advantage of Sass's `default!`([#2404](https://github.com/railsadminteam/rails_admin/pull/2404)) +- Proxy classes now inherit from BasicObject([#2434](https://github.com/railsadminteam/rails_admin/issues/2434)) +- Show sidebar scrollbar only on demand([#2419](https://github.com/railsadminteam/rails_admin/pull/2419)) +- RailsAdmin no longer gets excluded from NewRelic instrumentation by default([#2402](https://github.com/railsadminteam/rails_admin/pull/2402)) +- Improve efficiency of filter query in Postgres([#2401](https://github.com/railsadminteam/rails_admin/pull/2401)) +- Replace old jQueryUI datepicker with jQuery Bootstrap datetimepicker ([#2391](https://github.com/railsadminteam/rails_admin/pull/2391)) +- Turn Hash#symbolize into a helper to prevent namespace conflict([#2388](https://github.com/railsadminteam/rails_admin/pull/2388)) ### Removed -- The L10n translation `admin.misc.filter_date_format` datepicker search filters, has been dropped in favor of field oriented configuration ([#2391](https://github.com/sferik/rails_admin/pull/2391)) + +- The L10n translation `admin.misc.filter_date_format` datepicker search filters, has been dropped in favor of field oriented configuration ([#2391](https://github.com/railsadminteam/rails_admin/pull/2391)) ### Fixed -- AR#count broke when default-scoped with select([#2129](https://github.com/sferik/rails_admin/pull/2129), [#2447](https://github.com/sferik/rails_admin/issues/2447)) -- Datepicker could not handle Spanish date properly([#982](https://github.com/sferik/rails_admin/issues/982), [#2451](https://github.com/sferik/rails_admin/pull/2451)) -- Paperclip's `attachment_definitions` does not exist unless `has_attached_file`-ed([#1674](https://github.com/sferik/rails_admin/issues/1674)) -- `.btn` class was used without a modifier([#2417](https://github.com/sferik/rails_admin/pull/2417)) -- Filtering-multiselect widget ignored order([#2231](https://github.com/sferik/rails_admin/issues/2231), [#2412](https://github.com/sferik/rails_admin/pull/2412)) -- Add missing .alert-dismissible class to flash([#2411](https://github.com/sferik/rails_admin/pull/2411)) -- Keep field order on changing the existing field's type([#2409](https://github.com/sferik/rails_admin/pull/2409)) -- Add button for nested-many form in modal disappeared on click([#2372](https://github.com/sferik/rails_admin/issues/2372), [#2383](https://github.com/sferik/rails_admin/pull/2383)) + +- AR#count broke when default-scoped with select([#2129](https://github.com/railsadminteam/rails_admin/pull/2129), [#2447](https://github.com/railsadminteam/rails_admin/issues/2447)) +- Datepicker could not handle Spanish date properly([#982](https://github.com/railsadminteam/rails_admin/issues/982), [#2451](https://github.com/railsadminteam/rails_admin/pull/2451)) +- Paperclip's `attachment_definitions` does not exist unless `has_attached_file`-ed([#1674](https://github.com/railsadminteam/rails_admin/issues/1674)) +- `.btn` class was used without a modifier([#2417](https://github.com/railsadminteam/rails_admin/pull/2417)) +- Filtering-multiselect widget ignored order([#2231](https://github.com/railsadminteam/rails_admin/issues/2231), [#2412](https://github.com/railsadminteam/rails_admin/pull/2412)) +- Add missing .alert-dismissible class to flash([#2411](https://github.com/railsadminteam/rails_admin/pull/2411)) +- Keep field order on changing the existing field's type([#2409](https://github.com/railsadminteam/rails_admin/pull/2409)) +- Add button for nested-many form in modal disappeared on click([#2372](https://github.com/railsadminteam/rails_admin/issues/2372), [#2383](https://github.com/railsadminteam/rails_admin/pull/2383)) ### Security -- Fix XSS vulnerability in polymorphic select([#2479](https://github.com/sferik/rails_admin/pull/2479)) +- Fix XSS vulnerability in polymorphic select([#2479](https://github.com/railsadminteam/rails_admin/pull/2479)) -## [0.7.0](https://github.com/sferik/rails_admin/tree/v0.7.0) - 2015-08-16 +## [0.7.0](https://github.com/railsadminteam/rails_admin/tree/v0.7.0) - 2015-08-16 -[Full Changelog](https://github.com/sferik/rails_admin/compare/v0.6.8...v0.7.0) +[Full Changelog](https://github.com/railsadminteam/rails_admin/compare/v0.6.8...v0.7.0) ### Added -- Support for ActiveRecord::Enum ([#1993](https://github.com/sferik/rails_admin/issues/1993)) -- Multiselect-widget shows user friendly message, instead of just being blank ([#1369](https://github.com/sferik/rails_admin/issues/1369), [#2360](https://github.com/sferik/rails_admin/pull/2360)) -- Configuration option to turn browser validation off ([#2339](https://github.com/sferik/rails_admin/pull/2339), [#2373](https://github.com/sferik/rails_admin/pull/2373)) + +- Support for ActiveRecord::Enum ([#1993](https://github.com/railsadminteam/rails_admin/issues/1993)) +- Multiselect-widget shows user friendly message, instead of just being blank ([#1369](https://github.com/railsadminteam/rails_admin/issues/1369), [#2360](https://github.com/railsadminteam/rails_admin/pull/2360)) +- Configuration option to turn browser validation off ([#2339](https://github.com/railsadminteam/rails_admin/pull/2339), [#2373](https://github.com/railsadminteam/rails_admin/pull/2373)) ### Changed -- Multiselect-widget inserts a new item to the bottom, instead of top ([#2167](https://github.com/sferik/rails_admin/pull/2167)) -- Migrated Cerulean theme to Bootstrap3 ([#2352](https://github.com/sferik/rails_admin/pull/2352)) -- Better html markup for input fields ([#2336](https://github.com/sferik/rails_admin/pull/2336)) -- Update filter dropdown button to Bootstrap3 ([#2277](https://github.com/sferik/rails_admin/pull/2277)) -- Improve navbar appearance ([#2310](https://github.com/sferik/rails_admin/pull/2310)) -- Do not monkey patch the app's YAML ([#2331](https://github.com/sferik/rails_admin/pull/2331)) + +- Multiselect-widget inserts a new item to the bottom, instead of top ([#2167](https://github.com/railsadminteam/rails_admin/pull/2167)) +- Migrated Cerulean theme to Bootstrap3 ([#2352](https://github.com/railsadminteam/rails_admin/pull/2352)) +- Better html markup for input fields ([#2336](https://github.com/railsadminteam/rails_admin/pull/2336)) +- Update filter dropdown button to Bootstrap3 ([#2277](https://github.com/railsadminteam/rails_admin/pull/2277)) +- Improve navbar appearance ([#2310](https://github.com/railsadminteam/rails_admin/pull/2310)) +- Do not monkey patch the app's YAML ([#2331](https://github.com/railsadminteam/rails_admin/pull/2331)) ### Fixed -- Browser validation prevented saving of persisted upload fields ([#2376](https://github.com/sferik/rails_admin/issues/2376)) -- Fix inconsistent styling in static_navigation ([#2378](https://github.com/sferik/rails_admin/pull/2378)) -- Fix css regression for has_one and has_many nested form ([#2337](https://github.com/sferik/rails_admin/pull/2337)) -- HTML string inputs should not have a size attribute valorized with 0 ([#2335](https://github.com/sferik/rails_admin/pull/2335)) + +- Browser validation prevented saving of persisted upload fields ([#2376](https://github.com/railsadminteam/rails_admin/issues/2376)) +- Fix inconsistent styling in static_navigation ([#2378](https://github.com/railsadminteam/rails_admin/pull/2378)) +- Fix css regression for has_one and has_many nested form ([#2337](https://github.com/railsadminteam/rails_admin/pull/2337)) +- HTML string inputs should not have a size attribute valorized with 0 ([#2335](https://github.com/railsadminteam/rails_admin/pull/2335)) ### Security + - Fix XSS vulnerability in filtering-select widget -- Fix XSS vulnerability in association fields ([#2343](https://github.com/sferik/rails_admin/issues/2343)) +- Fix XSS vulnerability in association fields ([#2343](https://github.com/railsadminteam/rails_admin/issues/2343)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 32069799e9..0c6bfbf1bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,45 +5,75 @@ improve this project. [free-sw]: http://www.fsf.org/licensing/essays/free-sw.html -Here are some ways *you* can contribute: +Here are some ways _you_ can contribute: -* by using alpha, beta, and prerelease versions -* by triaging bug reports -* by writing or editing documentation -* by writing specifications -* by writing code (**no patch is too small**: fix typos, add comments, clean up +- by using alpha, beta, and prerelease versions +- by triaging bug reports +- by writing or editing documentation +- by writing specifications +- by writing code (**no patch is too small**: fix typos, add comments, clean up inconsistent whitespace) -* by refactoring code -* by fixing [issues][] -* by reviewing patches -* by suggesting new features -* [financially][gittip] +- by refactoring code +- by fixing [issues][] +- by reviewing patches +- by suggesting new features +- [financially][gittip] -[issues]: https://github.com/sferik/rails_admin/issues +[issues]: https://github.com/railsadminteam/rails_admin/issues [gittip]: https://www.gittip.com/sferik/ ## Development ### Running tests locally -To run the test suite, you need PhantomJS and ImageMagick (or GraphicsMagick). +To run the test suite, you need Google Chrome and ImageMagick (or GraphicsMagick). -Example on macOS using Homebrew: +Example installation on macOS using Homebrew: - brew cask install phantomjs - brew install imagemagick - bundle exec rspec +```bash +# install imagemagick: +brew install imagemagick +# install google chrome with cask: +brew install brew-cask +brew cask install google-chrome +``` + +Example installation on Ubuntu: + +```bash +# install imagemagick: +sudo apt update -y && sudo apt install imagemagick -y +# install google chrome: +sudo apt update -y && wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && sudo apt install ./google-chrome-stable_current_amd64.deb -y +``` + +Then you need to do this one-time setup: + +```bash +bundle install +yarn install +# install dependencies for each appraisal: +bundle exec appraisal install +# precompile assets in the dummy app: +cd spec/dummy_app && yarn install && yarn build && yarn build:css && cd - +``` + +Then you will be able to run the specs: + +```bash +bundle exec appraisal rails-7.0 rspec +``` ### Tests run against multiple versions of Rails -In CI, we use [appraisal] to run tests against multiple versions of Rails. -The [gemfiles/ directory] contains the Gemfiles used to create the CI build -matrix. See [the Travis CI configuration] for more details on what Ruby versions +In CI, we use [appraisal] to run tests against multiple versions of Rails. The +[gemfiles/ directory] contains the Gemfiles used to create the CI build matrix. +See the [GitHub Actions configuration] for more details on what Ruby versions run what Rails versions. [appraisal]: https://github.com/thoughtbot/appraisal [gemfiles/ directory]: ./gemfiles -[the Travis CI configuration]: ./.travis.yml +[github actions configuration]: ./.github/workflows/test.yml ## Getting Help @@ -59,13 +89,14 @@ rails_admin, please [open an issue][issues], but check to make sure it hasn't already been submitted. When submitting a bug report, please include a [Gist][] that includes a stack trace and any details that may be necessary to reproduce the bug, including your gem version, Ruby -version, and operating system. Ideally, a bug report should include a +version, and operating system. Ideally, a bug report should include a pull request with failing specs. [gist]: https://gist.github.com/ [list]: http://groups.google.com/group/rails_admin ## Submitting a Pull Request + 1. [Fork the repository.][fork] 2. Create a branch. 3. Add specs for your unimplemented feature or bug fix. diff --git a/Gemfile b/Gemfile index 42f96c2ea6..904083a026 100644 --- a/Gemfile +++ b/Gemfile @@ -1,18 +1,16 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gem 'appraisal', '>= 2.0' +gem 'devise', '~> 4.7' +gem 'net-smtp', require: false gem 'rails' -gem 'haml' -gem 'devise' - -group :active_record do - gem 'paper_trail' - - platforms :ruby, :mswin, :mingw, :x64_mingw do - gem 'mysql2', '>= 0.3.14' - gem 'sqlite3', '>= 1.3' - end -end +gem 'sassc-rails', '~> 2.1' +gem 'turbo-rails' +gem 'vite_rails', require: false +gem 'webpacker', require: false +gem 'webrick' group :development, :test do gem 'pry', '>= 0.9' @@ -21,30 +19,38 @@ end group :test do gem 'cancancan', '~> 3.0' gem 'carrierwave', ['>= 2.0.0.rc', '< 3'] - gem 'database_cleaner', ['>= 1.2', '!= 1.4.0', '!= 1.5.0', '< 2.0'] + gem 'cuprite', '!= 0.15.1' + gem 'database_cleaner-active_record', '>= 2.0', require: false gem 'dragonfly', '~> 1.0' - gem 'factory_bot', '>= 4.2' + gem 'factory_bot', '>= 4.2', '!= 6.4.5' gem 'generator_spec', '>= 0.8' + gem 'kt-paperclip' gem 'launchy', '>= 2.2' gem 'mini_magick', '>= 3.4' - gem 'poltergeist', '~> 1.5' gem 'pundit' gem 'rack-cache', require: 'rack/cache' - gem 'rspec-rails', '>= 2.14' gem 'rspec-expectations', '!= 3.8.3' + gem 'rspec-rails', '>= 4.0.0.beta2' gem 'rspec-retry' - gem 'rubocop', '~> 0.68.1', require: false + gem 'rubocop', ['~> 1.20', '!= 1.22.2'], require: false gem 'rubocop-performance', require: false + gem 'shrine', '~> 3.0' gem 'simplecov', '>= 0.9', require: false gem 'simplecov-lcov', require: false gem 'timecop', '>= 0.5' - platforms :ruby_19 do - gem 'tins', '~> 1.6.0', require: false - end - # Windows does not include zoneinfo files, so bundle the tzinfo-data gem - gem 'tzinfo-data', platforms: %i(mingw mswin x64_mingw jruby) + gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] +end + +group :active_record do + gem 'paper_trail', '>= 12.0' + + platforms :ruby, :mswin, :mingw, :x64_mingw do + gem 'mysql2', '>= 0.3.14' + gem 'pg', '>= 1.0.0' + gem 'sqlite3', '~> 1.3' + end end gemspec diff --git a/README.md b/README.md index fd425d078e..104726b85e 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,67 @@ # RailsAdmin [![Gem Version](https://img.shields.io/gem/v/rails_admin.svg)][gem] -[![Build Status](https://img.shields.io/github/workflow/status/sferik/rails_admin/Test)][ghactions] -[![Coverage Status](https://img.shields.io/coveralls/sferik/rails_admin.svg)][coveralls] -[![Inline docs](http://inch-ci.org/github/sferik/rails_admin.svg)][inch] -[![Code Climate](https://codeclimate.com/github/sferik/rails_admin.svg)][codeclimate] +[![Build Status](https://github.com/railsadminteam/rails_admin/actions/workflows/test.yml/badge.svg)][ghactions] +[![Coverage Status](https://img.shields.io/coveralls/railsadminteam/rails_admin.svg)][coveralls] +[![Code Climate](https://codeclimate.com/github/railsadminteam/rails_admin.svg)][codeclimate] [![SemVer](https://api.dependabot.com/badges/compatibility_score?dependency-name=rails_admin&package-manager=bundler&version-scheme=semver)][semver] [gem]: https://rubygems.org/gems/rails_admin -[ghactions]: https://github.com/sferik/rails_admin/actions -[coveralls]: https://coveralls.io/r/sferik/rails_admin -[inch]: http://inch-ci.org/github/sferik/rails_admin -[codeclimate]: https://codeclimate.com/github/sferik/rails_admin +[ghactions]: https://github.com/railsadminteam/rails_admin/actions/workflows/test.yml +[coveralls]: https://coveralls.io/r/railsadminteam/rails_admin +[codeclimate]: https://codeclimate.com/github/railsadminteam/rails_admin [semver]: https://dependabot.com/compatibility-score.html?dependency-name=rails_admin&package-manager=bundler&version-scheme=semver RailsAdmin is a Rails engine that provides an easy-to-use interface for managing your data. -## Announcements - -### [Action required] Security issue - -**RailsAdmin 2.0.1, 2.0.0 and up to 1.4.2 have been reported to have XSS vulnerability.** We strongly recommend that you upgrade RailsAdmin to 2.0.2 (and higher) or 1.4.3 as soon as possible, if you are on those versions. See [d72090ec](https://github.com/sferik/rails_admin/commit/d72090ec6a07c3b9b7b48ab50f3d405f91ff4375) for the detail. - ## Getting started -* Check out [the docs][docs]. -* Try the [live demo][demo]. ([Source code][dummy_app]) +- Check out [the docs][docs]. +- Try the [live demo][demo]. ([Source code][dummy_app]) -[demo]: http://rails-admin-tb.herokuapp.com/ -[dummy_app]: https://github.com/bbenezech/dummy_app -[docs]: https://github.com/sferik/rails_admin/wiki +[demo]: https://rails-admin.fly.dev/admin/ +[dummy_app]: https://github.com/railsadminteam/rails_admin/tree/master/spec/dummy_app +[docs]: https://github.com/railsadminteam/rails_admin/wiki ## Features -* CRUD any data with ease -* Custom actions -* Automatic form validation -* Search and filtering -* Export data to CSV/JSON/XML -* Authentication (via [Devise](https://github.com/plataformatec/devise) or other) -* Authorization (via [CanCanCan](https://github.com/CanCanCommunity/cancancan) or [Pundit](https://github.com/elabs/pundit)) -* User action history (via [PaperTrail](https://github.com/airblade/paper_trail)) -* Supported ORMs - * ActiveRecord - * Mongoid - +- CRUD any data with ease +- Custom actions +- Automatic form validation +- Search and filtering +- Export data to CSV/JSON/XML +- Authentication (via [Devise](https://github.com/plataformatec/devise) or other) +- Authorization (via [CanCanCan](https://github.com/CanCanCommunity/cancancan) or [Pundit](https://github.com/elabs/pundit)) +- User action history (via [PaperTrail](https://github.com/airblade/paper_trail)) +- Supported ORMs + - ActiveRecord + - Mongoid ## Installation -1. On your gemfile: `gem 'rails_admin', '~> 2.0'` +1. On your gemfile: `gem 'rails_admin', '~> 3.0'` 2. Run `bundle install` 3. Run `rails g rails_admin:install` 4. Provide a namespace for the routes when asked 5. Start a server `rails s` and administer your data at [/admin](http://localhost:3000/admin). (if you chose default namespace: /admin) +## Upgrading from 2.x + +Due to introduction of Webpack/Webpacker support, some additional dependency and configuration will be needed. +Running `rails g rails_admin:install` will suggest you some required changes, based on current setup of your app. + ## Configuration + ### Global + In `config/initializers/rails_admin.rb`: -[Details](https://github.com/sferik/rails_admin/wiki/Base-configuration) +[Details](https://github.com/railsadminteam/rails_admin/wiki/Base-configuration) -To begin with, you may be interested in setting up [Devise](https://github.com/sferik/rails_admin/wiki/Devise), [CanCanCan](https://github.com/sferik/rails_admin/wiki/Cancancan) or [Papertrail](https://github.com/sferik/rails_admin/wiki/Papertrail)! +To begin with, you may be interested in setting up [Devise](https://github.com/railsadminteam/rails_admin/wiki/Devise), [CanCanCan](https://github.com/railsadminteam/rails_admin/wiki/Cancancan) or [Papertrail](https://github.com/railsadminteam/rails_admin/wiki/Papertrail)! ### Per model + ```ruby class Ball < ActiveRecord::Base validates :name, presence: true @@ -76,31 +75,31 @@ class Ball < ActiveRecord::Base end ``` -Details: [Models](https://github.com/sferik/rails_admin/wiki/Models), [Groups](https://github.com/sferik/rails_admin/wiki/Groups), [Fields](https://github.com/sferik/rails_admin/wiki/Fields) +Details: [Models](https://github.com/railsadminteam/rails_admin/wiki/Models), [Groups](https://github.com/railsadminteam/rails_admin/wiki/Groups), [Fields](https://github.com/railsadminteam/rails_admin/wiki/Fields) ## Support + If you have a question, please check this README, the wiki, and the [list of known issues][troubleshoot]. -[troubleshoot]: https://github.com/sferik/rails_admin/wiki/Troubleshoot +[troubleshoot]: https://github.com/railsadminteam/rails_admin/wiki/Troubleshoot If you still have a question, you can ask the [official RailsAdmin mailing list][list]. [list]: http://groups.google.com/group/rails_admin -If you think you found a bug in RailsAdmin, you can [submit an issue](https://github.com/sferik/rails_admin/issues/new). +If you think you found a bug in RailsAdmin, you can [submit an issue](https://github.com/railsadminteam/rails_admin/issues/new). ## Supported Ruby Versions + This library aims to support and is [tested against][ghactions] the following Ruby implementations: -* Ruby 2.2 -* Ruby 2.3 -* Ruby 2.4 -* Ruby 2.5 -* Ruby 2.6 -* Ruby 2.7 -* Ruby 3.0 -* [JRuby][] +- Ruby 2.6 +- Ruby 2.7 +- Ruby 3.0 +- Ruby 3.1 +- Ruby 3.2 +- [JRuby][] [jruby]: http://jruby.org/ diff --git a/Rakefile b/Rakefile index 3de4b0a03a..25cf3f967a 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. @@ -17,8 +19,19 @@ begin rescue LoadError desc 'Run RuboCop' task :rubocop do - $stderr.puts 'Rubocop is disabled' + warn 'Rubocop is disabled' end end -task default: [:spec, :rubocop] +task default: %i[spec rubocop] + +namespace :vendorize do + desc 'Tasks for vendorizing assets' + + task :flatpickr do + Dir.chdir(__dir__) + flatpickr = File.read('node_modules/flatpickr/dist/flatpickr.js') + locales = Dir.glob('node_modules/flatpickr/dist/l10n/*.js').map { |f| File.read(f) } + File.write('vendor/assets/javascripts/rails_admin/flatpickr-with-locales.js', ([flatpickr] + locales).join("\n")) + end +end diff --git a/app/assets/images/rails_admin/aristo/images/bg_fallback.png b/app/assets/images/rails_admin/aristo/images/bg_fallback.png deleted file mode 100644 index 155ebd7e93..0000000000 Binary files a/app/assets/images/rails_admin/aristo/images/bg_fallback.png and /dev/null differ diff --git a/app/assets/images/rails_admin/aristo/images/icon_sprite.png b/app/assets/images/rails_admin/aristo/images/icon_sprite.png deleted file mode 100644 index 0b376bf66c..0000000000 Binary files a/app/assets/images/rails_admin/aristo/images/icon_sprite.png and /dev/null differ diff --git a/app/assets/images/rails_admin/aristo/images/progress_bar.gif b/app/assets/images/rails_admin/aristo/images/progress_bar.gif deleted file mode 100644 index c3d43fa40b..0000000000 Binary files a/app/assets/images/rails_admin/aristo/images/progress_bar.gif and /dev/null differ diff --git a/app/assets/images/rails_admin/aristo/images/slider_handles.png b/app/assets/images/rails_admin/aristo/images/slider_handles.png deleted file mode 100644 index 872eaac305..0000000000 Binary files a/app/assets/images/rails_admin/aristo/images/slider_handles.png and /dev/null differ diff --git a/app/assets/images/rails_admin/aristo/images/ui-icons_222222_256x240.png b/app/assets/images/rails_admin/aristo/images/ui-icons_222222_256x240.png deleted file mode 100644 index 664494d838..0000000000 Binary files a/app/assets/images/rails_admin/aristo/images/ui-icons_222222_256x240.png and /dev/null differ diff --git a/app/assets/images/rails_admin/aristo/images/ui-icons_454545_256x240.png b/app/assets/images/rails_admin/aristo/images/ui-icons_454545_256x240.png deleted file mode 100644 index 061a3b82a7..0000000000 Binary files a/app/assets/images/rails_admin/aristo/images/ui-icons_454545_256x240.png and /dev/null differ diff --git a/app/assets/images/rails_admin/bullet_black.png b/app/assets/images/rails_admin/bullet_black.png deleted file mode 100644 index d0a5e38f18..0000000000 Binary files a/app/assets/images/rails_admin/bullet_black.png and /dev/null differ diff --git a/app/assets/images/rails_admin/bullet_white.png b/app/assets/images/rails_admin/bullet_white.png deleted file mode 100644 index b63bab2233..0000000000 Binary files a/app/assets/images/rails_admin/bullet_white.png and /dev/null differ diff --git a/app/assets/images/rails_admin/calendar.png b/app/assets/images/rails_admin/calendar.png deleted file mode 100644 index 1829ffd6b2..0000000000 Binary files a/app/assets/images/rails_admin/calendar.png and /dev/null differ diff --git a/app/assets/images/rails_admin/clock.png b/app/assets/images/rails_admin/clock.png deleted file mode 100644 index 69c3144572..0000000000 Binary files a/app/assets/images/rails_admin/clock.png and /dev/null differ diff --git a/app/assets/images/rails_admin/logo.png b/app/assets/images/rails_admin/logo.png deleted file mode 100644 index ccdd17b95c..0000000000 Binary files a/app/assets/images/rails_admin/logo.png and /dev/null differ diff --git a/app/assets/images/rails_admin/magnifier.png b/app/assets/images/rails_admin/magnifier.png deleted file mode 100755 index 8893f7d71b..0000000000 Binary files a/app/assets/images/rails_admin/magnifier.png and /dev/null differ diff --git a/app/assets/images/rails_admin/multiselect/icon_sprite.png b/app/assets/images/rails_admin/multiselect/icon_sprite.png deleted file mode 100644 index 0b376bf66c..0000000000 Binary files a/app/assets/images/rails_admin/multiselect/icon_sprite.png and /dev/null differ diff --git a/app/assets/images/rails_admin/multiselect/ui-icon-circle-triangle-n-dark.png b/app/assets/images/rails_admin/multiselect/ui-icon-circle-triangle-n-dark.png deleted file mode 100644 index 715325382e..0000000000 Binary files a/app/assets/images/rails_admin/multiselect/ui-icon-circle-triangle-n-dark.png and /dev/null differ diff --git a/app/assets/images/rails_admin/multiselect/ui-icon-circle-triangle-n-light.png b/app/assets/images/rails_admin/multiselect/ui-icon-circle-triangle-n-light.png deleted file mode 100644 index b4de0286cc..0000000000 Binary files a/app/assets/images/rails_admin/multiselect/ui-icon-circle-triangle-n-light.png and /dev/null differ diff --git a/app/assets/images/rails_admin/multiselect/ui-icon-circle-triangle-s-dark.png b/app/assets/images/rails_admin/multiselect/ui-icon-circle-triangle-s-dark.png deleted file mode 100644 index c6f520724c..0000000000 Binary files a/app/assets/images/rails_admin/multiselect/ui-icon-circle-triangle-s-dark.png and /dev/null differ diff --git a/app/assets/images/rails_admin/multiselect/ui-icon-circle-triangle-s-light.png b/app/assets/images/rails_admin/multiselect/ui-icon-circle-triangle-s-light.png deleted file mode 100644 index 553cb4b9d9..0000000000 Binary files a/app/assets/images/rails_admin/multiselect/ui-icon-circle-triangle-s-light.png and /dev/null differ diff --git a/app/assets/javascripts/rails_admin/application.js.erb b/app/assets/javascripts/rails_admin/application.js.erb new file mode 100644 index 0000000000..012612e82d --- /dev/null +++ b/app/assets/javascripts/rails_admin/application.js.erb @@ -0,0 +1,30 @@ +//= require 'rails-ujs' +//= require 'turbo' +//= require 'rails_admin/jquery3' +//= require 'jquery_nested_form' +//= require 'rails_admin/jquery-ui/effect' +//= require 'rails_admin/jquery-ui/widgets/sortable' +//= require 'rails_admin/jquery-ui/widgets/autocomplete' +//= require 'rails_admin/flatpickr-with-locales' +//= require 'rails_admin/popper' +//= require 'rails_admin/bootstrap' + +//= require 'rails_admin/abstract-select' +//= require 'rails_admin/filter-box' +//= require 'rails_admin/filtering-multiselect' +//= require 'rails_admin/filtering-select' +//= require 'rails_admin/remote-form' +//= require 'rails_admin/nested-form-hooks' +//= require 'rails_admin/i18n' +//= require 'rails_admin/widgets' +//= require 'rails_admin/sidescroll' +//= require 'rails_admin/ui' +//= require 'rails_admin/custom/ui' + +<% if defined?(ActiveStorage::Engine) %> +//= require activestorage +<% end %> +<% if defined?(ActionText::Engine) && Rails.gem_version >= Gem::Version.new('7.0') %> +//= require trix +//= require actiontext +<% end %> diff --git a/app/assets/javascripts/rails_admin/jquery-ui.js.erb b/app/assets/javascripts/rails_admin/jquery-ui.js.erb deleted file mode 100644 index cce243bfa0..0000000000 --- a/app/assets/javascripts/rails_admin/jquery-ui.js.erb +++ /dev/null @@ -1,8 +0,0 @@ -<% require_asset "jquery-ui/effect" %> -<% if Jquery::Ui::Rails::JQUERY_UI_VERSION >= '1.12' %> - <% require_asset "jquery-ui/widgets/sortable" %> - <% require_asset "jquery-ui/widgets/autocomplete" %> -<% else %> - <% require_asset "jquery-ui/sortable" %> - <% require_asset "jquery-ui/autocomplete" %> -<% end %> diff --git a/app/assets/javascripts/rails_admin/ra.filter-box.js b/app/assets/javascripts/rails_admin/ra.filter-box.js deleted file mode 100644 index 7ddd32976c..0000000000 --- a/app/assets/javascripts/rails_admin/ra.filter-box.js +++ /dev/null @@ -1,248 +0,0 @@ -(function($) { - - var filters; - - $.filters = filters = { - append: function(options) { - options = options || {}; - var field_label = options['label']; - var field_name = options['name']; - var field_type = options['type']; - var field_value = options['value']; - var field_operator = options['operator']; - var select_options = options['select_options']; - var required = options['required']; - var index = options['index']; - var value_name = 'f[' + field_name + '][' + index + '][v]'; - var operator_name = 'f[' + field_name + '][' + index + '][o]'; - var control = null; - var additional_control = null; - - switch(field_type) { - case 'boolean': - control = $('') - .prop('name', value_name) - .append('') - .append($('').prop('selected', field_value == "true").text(RailsAdmin.I18n.t("true"))) - .append($('').prop('selected', field_value == "false").text(RailsAdmin.I18n.t("false"))) - if (!required) { - control.append([ - '', - $('').prop('selected', field_value == "_present").text(RailsAdmin.I18n.t("is_present")), - $('').prop('selected', field_value == "_blank").text(RailsAdmin.I18n.t("is_blank")) - ]) - } - break; - case 'date': - additional_control = - $('') - .css('display', (!field_operator || field_operator == "default") ? 'inline-block' : 'none') - .prop('name', value_name + '[]') - .prop('value', field_value[0] || '') - .add( - $('') - .css('display', (field_operator == "between") ? 'inline-block' : 'none') - .prop('name', value_name + '[]') - .prop('value', field_value[1] || '') - ) - .add( - $('') - .css('display', (field_operator == "between") ? 'inline-block' : 'none') - .prop('name', value_name + '[]') - .prop('value', field_value[2] || '') - ); - case 'datetime': - case 'timestamp': - control = control || $('') - .prop('name', operator_name) - .append($('').prop('selected', field_operator == "default").text(RailsAdmin.I18n.t("date"))) - .append($('').prop('selected', field_operator == "between").text(RailsAdmin.I18n.t("between_and_"))) - .append($('').prop('selected', field_operator == "today").text(RailsAdmin.I18n.t("today"))) - .append($('').prop('selected', field_operator == "yesterday").text(RailsAdmin.I18n.t("yesterday"))) - .append($('').prop('selected', field_operator == "this_week").text(RailsAdmin.I18n.t("this_week"))) - .append($('').prop('selected', field_operator == "last_week").text(RailsAdmin.I18n.t("last_week"))) - if (!required) { - control.append([ - '', - $('').prop('selected', field_operator == "_not_null").text(RailsAdmin.I18n.t("is_present")), - $('').prop('selected', field_operator == "_null").text(RailsAdmin.I18n.t("is_blank")) - ]) - } - additional_control = additional_control || - $('') - .css('display', (!field_operator || field_operator == "default") ? 'inline-block' : 'none') - .prop('name', value_name + '[]') - .prop('value', field_value[0] || '') - .add( - $('') - .css('display', (field_operator == "between") ? 'inline-block' : 'none') - .prop('name', value_name + '[]') - .prop('value', field_value[1] || '') - ) - .add( - $('') - .css('display', (field_operator == "between") ? 'inline-block' : 'none') - .prop('name', value_name + '[]') - .prop('value', field_value[2] || '') - ); - break; - case 'enum': - var multiple_values = ((field_value instanceof Array) ? true : false) - control = $('') - .css('display', multiple_values ? 'none' : 'inline-block') - .prop('name', multiple_values ? undefined : value_name) - .data('name', value_name) - .append('') - if (!required) { - control.append([ - $('').prop('selected', field_value == "_present").text(RailsAdmin.I18n.t("is_present")), - $('').prop('selected', field_value == "_blank").text(RailsAdmin.I18n.t("is_blank")), - '' - ]) - } - control.append(select_options) - .add( - $('') - .css('display', multiple_values ? 'inline-block' : 'none') - .prop('name', multiple_values ? value_name + '[]' : undefined) - .data('name', value_name + '[]') - .append(select_options) - ) - .add( - $('') - .append($('').addClass('icon-' + (multiple_values ? 'minus' : 'plus'))) - ); - break; - case 'string': - case 'text': - case 'belongs_to_association': - control = $('') - .prop('value', field_operator) - .prop('name', operator_name) - .append('') - .append($('').prop('selected', field_operator == "like").text(RailsAdmin.I18n.t("contains"))) - .append($('').prop('selected', field_operator == "is").text(RailsAdmin.I18n.t("is_exactly"))) - .append($('').prop('selected', field_operator == "starts_with").text(RailsAdmin.I18n.t("starts_with"))) - .append($('').prop('selected', field_operator == "ends_with").text(RailsAdmin.I18n.t("ends_with"))) - if (!required) { - control.append([ - '', - $('').prop('selected', field_operator == "_present").text(RailsAdmin.I18n.t("is_present")), - $('').prop('selected', field_operator == "_blank").text(RailsAdmin.I18n.t("is_blank")) - ]) - } - additional_control = $('') - .css('display', field_operator == "_present" || field_operator == "_blank" ? 'none' : 'inline-block') - .prop('name', value_name) - .prop('value', field_value); - break; - case 'integer': - case 'decimal': - case 'float': - control = $('') - .prop('name', operator_name) - .append($('').prop('selected', field_operator == "default").text(RailsAdmin.I18n.t("number"))) - .append($('').prop('selected', field_operator == "between").text(RailsAdmin.I18n.t("between_and_"))) - if (!required) { - control.append([ - '', - $('').prop('selected', field_operator == "_not_null").text(RailsAdmin.I18n.t("is_present")), - $('').prop('selected', field_operator == "_null").text(RailsAdmin.I18n.t("is_blank")) - ]) - } - additional_control = - $('') - .css('display', (!field_operator || field_operator == "default") ? 'inline-block' : 'none') - .prop('type', field_type) - .prop('name', value_name + '[]') - .prop('value', field_value[0] || '') - .add( - $('') - .css('display', (field_operator == "between") ? 'inline-block' : 'none') - .prop('type', field_type) - .prop('name', value_name + '[]') - .prop('value', field_value[1] || '') - ) - .add( - $('') - .css('display', (field_operator == "between") ? 'inline-block' : 'none') - .prop('type', field_type) - .prop('name', value_name + '[]') - .prop('value', field_value[2] || '') - ); - break; - default: - control = $('') - .prop('name', value_name) - .prop('value', field_value); - break; - } - - var filterContainerId = field_name + '-' + index + '-filter-container'; - $('p#' + filterContainerId).remove(); - - var $content = $('

') - .attr('id', filterContainerId) - .addClass('filter form-search') - .append( - $('') - .append($('').append('').append(document.createTextNode(field_label))) - ) - .append(' ') - .append(control) - .append(' ') - .append(additional_control); - - $('#filters_box').append($content); - - $content.find('.date, .datetime').datetimepicker({ - locale: RailsAdmin.I18n.locale, - showTodayButton: true, - format: options['datetimepicker_format'] - }); - - $("hr.filters_box:hidden").show('slow'); - } - } - - $(document).on('click', "#filters a", function(e) { - e.preventDefault(); - $.filters.append({ - label: $(this).data('field-label'), - name: $(this).data('field-name'), - type: $(this).data('field-type'), - value: $(this).data('field-value'), - operator: $(this).data('field-operator'), - select_options: $(this).data('field-options'), - required: $(this).data('field-required'), - index: $.now().toString().slice(6,11), - datetimepicker_format: $(this).data('field-datetimepicker-format') - }); - }); - - $(document).on('click', "#filters_box .delete", function(e) { - e.preventDefault(); - form = $(this).parents('form'); - $(this).parents('.filter').remove(); - !$("#filters_box").children().length && $("hr.filters_box:visible").hide('slow'); - }); - - $(document).on('click', "#filters_box .switch-select", function(e) { - e.preventDefault(); - var selected_select = $(this).siblings('select:visible'); - var not_selected_select = $(this).siblings('select:hidden'); - not_selected_select.attr('name', not_selected_select.data('name')).show('slow'); - selected_select.attr('name', null).hide('slow'); - $(this).find('i').toggleClass("icon-plus icon-minus") - }); - - $(document).on('change', "#filters_box .switch-additional-fieldsets", function(e) { - var selected_option = $(this).find('option:selected'); - if(klass = $(selected_option).data('additional-fieldset')) { - $(this).siblings('.additional-fieldset:not(.' + klass + ')').hide('slow'); - $(this).siblings('.' + klass).show('slow'); - } else { - $(this).siblings('.additional-fieldset').hide('slow'); - } - }); -})( jQuery ); diff --git a/app/assets/javascripts/rails_admin/ra.filtering-multiselect.js b/app/assets/javascripts/rails_admin/ra.filtering-multiselect.js deleted file mode 100644 index 172190df1b..0000000000 --- a/app/assets/javascripts/rails_admin/ra.filtering-multiselect.js +++ /dev/null @@ -1,331 +0,0 @@ -/* - * RailsAdmin filtering multiselect @VERSION - * - * License - * - * http://www.railsadmin.org - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js - */ -(function($) { - $.widget("ra.filteringMultiselect", { - _cache: {}, - options: { - createQuery: function(query) { - return { query: query }; - }, - sortable: false, - removable: true, - regional: { - add: 'Add', - chooseAll: 'Choose all', - clearAll: 'Clear all', - down: 'Down', - remove: 'Remove', - search: 'Search', - up: 'Up' - }, - searchDelay: 400, - remote_source: null, - xhr: false - }, - - _create: function() { - this._cache = {}; - this._build(); - this._buildCache(); - this._bindEvents(); - }, - - _build: function() { - var i; - - this.wrapper = $('

'); - - this.wrapper.insertAfter(this.element); - - this.header = $('
'); - - this.filter = $(''); - - this.header.append(this.filter); - - this.wrapper.append(this.header); - - this.columns = { - left: $('
'), - center: $('
'), - right: $('
') - }; - - for (i in this.columns) { - if (this.columns.hasOwnProperty(i)) { - this.wrapper.append(this.columns[i]); - } - } - - this.collection = $(''); - - this.collection.addClass("form-control ra-multiselect-collection"); - - this.addAll = $('' + this.options.regional.chooseAll + ''); - - this.columns.left.html(this.collection) - .append(this.addAll); - - this.collection.wrap('
'); - - - this.add = $('' + this.options.regional.add + ''); - this.columns.center.append(this.add); - - if (this.options.removable) { - this.remove = $('' + this.options.regional.remove + ''); - this.columns.center.append(this.remove); - } - - if (this.options.sortable) { - this.up = $('' + this.options.regional.up + ''); - this.down = $('' + this.options.regional.down + ''); - this.columns.center.append(this.up).append(this.down); - } - - this.selection = $(''); - this.columns.right.append(this.selection); - - - if (this.options.removable) { - this.removeAll = $('' + this.options.regional.clearAll + ''); - this.columns.right.append(this.removeAll); - } - - this.selection.wrap('
'); - - this.element.css({display: "none"}); - - this.tooManyObjectsPlaceholder = $('') - .prop('value', matches[filtered[i]].id) - .prop('title', matches[filtered[i]].label) - .text(matches[filtered[i]].label); - $(widget.collection[0]).append(newOptions); - } - } else { - widget.collection[0].innerHTML = widget.noObjectsPlaceholder; - } - }); - }, - - /* - * Cache key is stored in the format `o_').prop('value', option.value).prop('selected', true)); - } - }); - $(options).appendTo(this.selection).prop('selected', false); - }, - - _move: function(direction, options) { - var widget = this; - if(direction == 'up') { - options.each(function(i, option) { - var prev = $(option).prev(); - if (prev.length > 0) { - var el = widget.element.find('option[value="' + option.value + '"]'); - var el_prev = widget.element.find('option[value="' + prev[0].value + '"]'); - el_prev.before(el); - prev.before($(option)); - } - }); - } else { - $.fn.reverse = [].reverse; // needed to lower last items first - options.reverse().each(function(i, option) { - var next = $(option).next(); - if (next.length > 0) { - var el = widget.element.find('option[value="' + option.value + '"]'); - var el_next = widget.element.find('option[value="' + next[0].value + '"]'); - el_next.after(el); - next.after($(option)); - } - }); - } - }, - - selected: function(value) { - if (this.selection[0].querySelectorAll('option[value="' + value + '"]')[0]) { - return true; - } - }, - - destroy: function() { - this.wrapper.remove(); - this.element.css({display: "inline"}); - $.Widget.prototype.destroy.apply(this, arguments); - } - }); -})(jQuery); diff --git a/app/assets/javascripts/rails_admin/ra.filtering-select.js b/app/assets/javascripts/rails_admin/ra.filtering-select.js deleted file mode 100644 index 93cd0ac79e..0000000000 --- a/app/assets/javascripts/rails_admin/ra.filtering-select.js +++ /dev/null @@ -1,296 +0,0 @@ -/* - * RailsAdmin filtering select @VERSION - * - * Based on the combobox example from jQuery UI documentation - * http://jqueryui.com/demos/autocomplete/#combobox - * - * License - * - * http://www.railsadmin.org - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js - * jquery.ui.autocomplete.js - */ -(function($) { - 'use strict'; - - $.widget('ra.filteringSelect', { - options: { - createQuery: function(query) { - return { query: query }; - }, - minLength: 0, - searchDelay: 200, - remote_source: null, - source: null, - float_left: true, - xhr: false - }, - - button: null, - input: null, - select: null, - filtering_select: null, - - _create: function() { - this.filtering_select = this.element.siblings( - '[data-input-for="' + this.element.attr('id') + '"]' - ); - - // When using the browser back and forward buttons, it is possible that - // the autocomplete field will be cached which causes duplicate fields - // to be generated. - if (this.filtering_select.length > 0) { - this.input = this.filtering_select.children('input'); - this.button = this.filtering_select.children('.input-group-btn'); - } else { - this.element.hide(); - this.filtering_select = this._inputGroup(this.element.attr('id')); - this.input = this._inputField(); - this.button = this._buttonField(); - } - - if (this.options.float_left) { - this.filtering_select.css('float', 'left') - } else { - this.filtering_select.css('float', 'none') - } - - this._setOptionsSource(); - this._initAutocomplete(); - this._initKeyEvent(); - this._overloadRenderItem(); - this._autocompleteDropdownEvent(this.button); - - return this.filtering_select.append(this.input) - .append(this.button) - .insertAfter(this.element); - }, - - _getResultSet: function(request, data, xhr) { - var matcher = new RegExp($.ui.autocomplete.escapeRegex(request.term), 'i'); - - var spannedContent = function(content) { - return $('').text(content).html(); - }; - - var highlighter = function(label, word) { - if(word.length) { - return $.map( - label.split(word), - function(el) { - return spannedContent(el); - }) - .join($('') - .text(word)[0] - .outerHTML - ); - } else { - return spannedContent(label); - } - }; - - return $.map( - data, - function(el) { - var id = el.id || el.value; - var value = el.label || el.id; - // match regexp only for local requests, remote ones are already - // filtered, and label may not contain filtered term. - if (id && (xhr || matcher.test(el.label))) { - return { - html: highlighter(value, request.term), - value: value, - id: id - }; - } - }); - }, - - _getSourceFunction: function(source) { - var self = this; - var requestIndex = 0; - - if ($.isArray(source)) { - return function(request, response) { - response(self._getResultSet(request, source, false)); - }; - } else if (typeof source === 'string') { - return function(request, response) { - if (this.xhr) { - this.xhr.abort(); - } - - this.xhr = $.ajax({ - url: source, - data: self.options.createQuery(request.term), - dataType: 'json', - autocompleteRequest: ++requestIndex, - success: function(data, status) { - if (this.autocompleteRequest === requestIndex) { - response(self._getResultSet(request, data, true)); - } - }, - error: function() { - if (this.autocompleteRequest === requestIndex) { - response([]); - } - } - }); - }; - } else { - return source; - } - }, - - _setOptionsSource: function() { - if (this.options.xhr) { - this.options.source = this.options.remote_source; - } else { - this.options.source = this.element.children('option').map(function() { - return { label: $(this).text(), value: this.value }; - }).toArray(); - } - }, - - _buttonField: function() { - return $( - '' + - '' + - '' - ); - }, - - _autocompleteDropdownEvent: function(element) { - var self = this; - - return element.click(function() { - // close if already visible - if (self.input.autocomplete('widget').is(':visible')) { - self.input.autocomplete('close'); - return; - } - - // pass empty string as value to search for, displaying all results - self.input.autocomplete('search', ''); - self.input.focus(); - }); - }, - - _inputField: function() { - var input; - var selected = this.element.children(':selected'); - var value = selected.val() ? selected.text() : ''; - - input = $('') - .val(value) - .addClass('form-control ra-filtering-select-input') - .attr('style', this.element.attr('style')) - .show(); - - if (this.element.attr('placeholder')) { - input.attr('placeholder', this.element.attr('placeholder')); - } - - return input; - }, - - _inputGroup: function(inputFor) { - return $('
') - .addClass('input-group filtering-select col-sm-2') - .attr('data-input-for', inputFor) - .css('float', this.element.css("float")); - }, - - _initAutocomplete: function() { - var self = this; - - return this.input.autocomplete({ - delay: this.options.searchDelay, - minLength: this.options.minLength, - source: this._getSourceFunction(this.options.source), - select: function(event, ui) { - var option = $('')); - self.input.data('ui-autocomplete').term = ''; - $(self.element.parents('.controls')[0]) - .find('.update') - .addClass('disabled'); - return false; - } - }); - }, - - _initKeyEvent: function() { - var self = this; - - return this.input.keyup(function() { - if ($(this).val().length) { - return; - } - - /* Clear select options and trigger change if selected item is deleted */ - return self.element - .html($('')) - .trigger('change'); - }); - }, - - _overloadRenderItem: function() { - this.input.data('ui-autocomplete')._renderItem = function(ul, item) { - return $('
  • ') - .data('ui-autocomplete-item', item) - .append($('') - .html(item.html || item.id)) - .appendTo(ul); - }; - }, - - destroy: function() { - this.input.remove(); - this.button.remove(); - this.element.show(); - this.filtering_select.remove(); - $.Widget.prototype.destroy.call(this); - } - }); -})(jQuery); diff --git a/app/assets/javascripts/rails_admin/ra.i18n.js b/app/assets/javascripts/rails_admin/ra.i18n.js deleted file mode 100644 index ab42470e4e..0000000000 --- a/app/assets/javascripts/rails_admin/ra.i18n.js +++ /dev/null @@ -1,28 +0,0 @@ -(function() { - var Locale; - - this.RailsAdmin || (this.RailsAdmin = {}); - - this.RailsAdmin.I18n = Locale = (function() { - function Locale() {} - - Locale.init = function(locale, translations) { - this.locale = locale; - this.translations = translations; - moment.locale(this.locale); - if (typeof this.translations === "string") { - this.translations = JSON.parse(this.translations); - } - }; - - Locale.t = function(key) { - var humanize; - humanize = key.charAt(0).toUpperCase() + key.replace(/_/g, " ").slice(1); - return this.translations[key] || humanize; - }; - - return Locale; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/rails_admin/ra.nested-form-hooks.js b/app/assets/javascripts/rails_admin/ra.nested-form-hooks.js deleted file mode 100644 index 8175335d45..0000000000 --- a/app/assets/javascripts/rails_admin/ra.nested-form-hooks.js +++ /dev/null @@ -1,59 +0,0 @@ -(function($) { - $(document).ready(function() { - return window.nestedFormEvents.insertFields = function(content, assoc, link) { - var tab_content; - tab_content = $(link).closest(".controls").siblings(".tab-content"); - tab_content.append(content); - return tab_content.children().last(); - }; - }); - - $(document).on('nested:fieldAdded', 'form', function(content) { - var controls, field, nav, new_tab, one_to_one, parent_group, toggler; - field = content.field.addClass('tab-pane').attr('id', 'unique-id-' + (new Date().getTime())); - new_tab = $('
  • ').append( - $('').attr('data-toggle', 'tab').attr('href', '#' + field.attr('id')).text(field.children('.object-infos').data('object-label')) - ) - parent_group = field.closest('.control-group'); - controls = parent_group.children('.controls'); - one_to_one = controls.data('nestedone') !== void 0; - nav = controls.children('.nav'); - content = parent_group.children('.tab-content'); - toggler = controls.find('.toggler'); - nav.append(new_tab); - $(window.document).trigger('rails_admin.dom_ready', [field, parent_group]); - new_tab.children('a').tab('show'); - if (!one_to_one) { - nav.select(':hidden').show('slow'); - } - content.select(':hidden').show('slow'); - toggler.addClass('active').removeClass('disabled').children('i').addClass('icon-chevron-down').removeClass('icon-chevron-right'); - if (one_to_one) { - controls.find('.add_nested_fields').removeClass('add_nested_fields').text(field.children('.object-infos').data('object-label')); - } - }); - - $(document).on('nested:fieldRemoved', 'form', function(content) { - var add_button, controls, current_li, field, nav, one_to_one, parent_group, toggler; - field = content.field; - nav = field.closest(".control-group").children('.controls').children('.nav'); - current_li = nav.children('li').has('a[href="#' + field.attr('id') + '"]'); - parent_group = field.closest(".control-group"); - controls = parent_group.children('.controls'); - one_to_one = controls.data('nestedone') !== void 0; - toggler = controls.find('.toggler'); - (current_li.next().length ? current_li.next() : current_li.prev()).children('a:first').tab('show'); - current_li.remove(); - if (nav.children().length === 0) { - nav.select(':visible').hide('slow'); - toggler.removeClass('active').addClass('disabled').children('i').removeClass('icon-chevron-down').addClass('icon-chevron-right'); - } - if (one_to_one) { - add_button = toggler.next(); - add_button.addClass('add_nested_fields').html(add_button.data('add-label')); - } - field.find('[required]').each(function() { - $(this).removeAttr('required'); - }); - }); -}(jQuery)); diff --git a/app/assets/javascripts/rails_admin/ra.remote-form.js b/app/assets/javascripts/rails_admin/ra.remote-form.js deleted file mode 100644 index 4bab8d4510..0000000000 --- a/app/assets/javascripts/rails_admin/ra.remote-form.js +++ /dev/null @@ -1,155 +0,0 @@ -/* - * RailsAdmin remote form @VERSION - * - * License - * - * http://www.railsadmin.org - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js - * jquery.ui.dialog.js - */ -(function($) { - $.widget("ra.remoteForm", { - - _create: function() { - var widget = this - var dom_widget = widget.element; - - var edit_url = dom_widget.find('select').first().data('options') && dom_widget.find('select').first().data('options')['edit-url']; - if(typeof(edit_url) != 'undefined' && edit_url.length) { - dom_widget.on('dblclick', '.ra-multiselect option:not(:disabled)', function(e){ - widget._bindModalOpening(e, edit_url.replace('__ID__', this.value)) - }); - } - - dom_widget.find('.create').unbind().bind("click", function(e){ - widget._bindModalOpening(e, $(this).data('link')) - }); - - dom_widget.find('.update').unbind().bind("click", function(e){ - if(value = dom_widget.find('select').val()) { - widget._bindModalOpening(e, $(this).data('link').replace('__ID__', value)) - } else { - e.preventDefault(); - } - }); - }, - - _bindModalOpening: function(e, url) { - e.preventDefault(); - widget = this; - if($("#modal").length) - return false; - - var dialog = this._getModal(); - - setTimeout(function(){ // fix race condition with modal insertion in the dom (Chrome => Team/add a new fan => #modal not found when it should have). Somehow .on('show') is too early, tried it too. - $.ajax({ - url: url, - beforeSend: function(xhr) { - xhr.setRequestHeader("Accept", "text/javascript"); - }, - success: function(data, status, xhr) { - dialog.find('.modal-body').html(data); - widget._bindFormEvents(); - }, - error: function(xhr, status, error) { - dialog.find('.modal-body').html(xhr.responseText); - }, - dataType: 'text' - }); - },200); - - }, - - _bindFormEvents: function() { - var widget = this, - dialog = this._getModal(), - form = dialog.find("form"), - saveButtonText = dialog.find(":submit[name=_save]").html(), - cancelButtonText = dialog.find(":submit[name=_continue]").html(); - dialog.find('.form-actions').remove(); - - form.attr("data-remote", true); - dialog.find('.modal-header-title').text(form.data('title')); - dialog.find('.cancel-action').unbind().click(function(){ - dialog.modal('hide'); - return false; - }).html(cancelButtonText); - - dialog.find('.save-action').unbind().click(function(){ - form.submit(); - return false; - }).html(saveButtonText); - - $(document).trigger('rails_admin.dom_ready', [form]) - - form.bind("ajax:complete", function(xhr, data, status) { - if (status == 'error') { - dialog.find('.modal-body').html(data.responseText); - widget._bindFormEvents(); - } else { - var json = $.parseJSON(data.responseText); - var option = ''; - var select = widget.element.find('select').filter(":hidden"); - - if(widget.element.find('.filtering-select').length) { // select input - var input = widget.element.find('.filtering-select').children('.ra-filtering-select-input'); - input.val(json.label); - if (!select.find('option[value=' + json.id + ']').length) { // not a replace - select.html(option).val(json.id); - widget.element.find('.update').removeClass('disabled'); - } - } else { // multi-select input - var input = widget.element.find('.ra-filtering-select-input'); - var multiselect = widget.element.find('.ra-multiselect'); - if (multiselect.find('option[value=' + json.id + ']').length) { // replace - select.find('option[value=' + json.id + ']').text(json.label); - multiselect.find('option[value= ' + json.id + ']').text(json.label); - } else { // add - select.append(option); - multiselect.find('select.ra-multiselect-selection').append(option); - } - } - widget._trigger("success"); - dialog.modal("hide"); - } - }); - }, - - _getModal: function() { - var widget = this; - if (!widget.dialog) { - widget.dialog = $('') - .modal({ - keyboard: true, - backdrop: true, - show: true - }) - .on('hidden.bs.modal', function(){ - widget.dialog.remove(); // We don't want to reuse closed modals - widget.dialog = null; - }); - } - return this.dialog; - } - }); -})(jQuery); diff --git a/app/assets/javascripts/rails_admin/ra.sidescroll.js b/app/assets/javascripts/rails_admin/ra.sidescroll.js deleted file mode 100644 index 7dfe516a8f..0000000000 --- a/app/assets/javascripts/rails_admin/ra.sidescroll.js +++ /dev/null @@ -1,31 +0,0 @@ -(function($) { - function setFrozenColPositions() { - var $listForm, frozenColumns; - - $listForm = $('#bulk_form'); - if (!$listForm.is('.ra-sidescroll')) { - return; - } - frozenColumns = $listForm.data('ra-sidescroll'); - - $listForm.find('table tr').each(function(index, tr) { - var firstPosition = 0; - - $(tr).children().slice(0, frozenColumns).each(function(idx, td) { - var tdLeft; - $(td).addClass('ra-sidescroll-frozen'); - if (idx === frozenColumns - 1) { - $(td).addClass('last-of-frozen'); - } - tdLeft = $(td).position().left; - if (idx === 0) { - firstPosition = tdLeft; - } - td.style.left = (tdLeft - firstPosition) + "px"; - }); - }); - }; - - $(window).on('load', setFrozenColPositions); - $(document).on('rails_admin.dom_ready', setFrozenColPositions); -})(jQuery); diff --git a/app/assets/javascripts/rails_admin/ra.widgets.js b/app/assets/javascripts/rails_admin/ra.widgets.js deleted file mode 100644 index 4662e2bcaa..0000000000 --- a/app/assets/javascripts/rails_admin/ra.widgets.js +++ /dev/null @@ -1,379 +0,0 @@ -(function($) { - $(document).on('rails_admin.dom_ready', function(e, content) { - var $editors, array, config_options, goBootstrapWysihtml5s, goCkeditors, goCodeMirrors, goFroalaWysiwygs, goSimpleMDEs, options; - content = content ? content : $('form'); - if (content.length) { - content.find('[data-color]').each(function() { - var that; - that = this; - return $(this).ColorPicker({ - color: $(that).val(), - onShow: function(el) { - $(el).fadeIn(500); - return false; - }, - onHide: function(el) { - $(el).fadeOut(500); - return false; - }, - onChange: function(hsb, hex, rgb) { - $(that).val(hex); - $(that).css('backgroundColor', '#' + hex); - } - }); - }); - $.fn.datetimepicker.defaults.icons = { - time: 'fa fa-clock-o', - date: 'fa fa-calendar', - up: 'fa fa-chevron-up', - down: 'fa fa-chevron-down', - previous: 'fa fa-angle-double-left', - next: 'fa fa-angle-double-right', - today: 'fa fa-dot-circle-o', - clear: 'fa fa-trash', - close: 'fa fa-times' - }; - content.find('[data-datetimepicker]').each(function() { - var options; - options = $(this).data('options'); - $.extend(options, { - locale: RailsAdmin.I18n.locale - }); - $(this).datetimepicker(options); - }); - content.find('[data-enumeration]').each(function() { - if ($(this).is('[multiple]')) { - $(this).filteringMultiselect($(this).data('options')); - } else { - $(this).filteringSelect($(this).data('options')); - } - }); - content.find('[data-fileupload]').each(function() { - var parent; - parent = $(this).closest('.controls'); - parent.find('.btn-remove-image').on('click', function() { - $(this).siblings('[type=checkbox]').click(); - parent.find('.toggle').toggle('slow'); - $(this).toggleClass('btn-danger btn-info'); - return false; - }); - }); - content.find('[data-fileupload]').change(function() { - var ext, image_container, input, reader; - input = this; - image_container = $("#" + input.id).parent().children(".preview"); - if (!image_container.length) { - image_container = $("#" + input.id).parent().prepend($('').addClass('preview').addClass('img-thumbnail')).find('img.preview'); - image_container.parent().find('img:not(.preview)').hide(); - } - ext = $("#" + input.id).val().split('.').pop().toLowerCase(); - if (input.files && input.files[0] && $.inArray(ext, ['gif', 'png', 'jpg', 'jpeg', 'bmp']) !== -1) { - reader = new FileReader(); - reader.onload = function(e) { - image_container.attr("src", e.target.result); - }; - reader.readAsDataURL(input.files[0]); - image_container.show(); - } else { - image_container.hide(); - } - }); - content.find('[data-multiple-fileupload]').each(function() { - $(this).closest('.controls').find('.btn-remove-image').on('click', function() { - $(this).siblings('[type=checkbox]').click(); - $(this).parent('.toggle').toggle('slow'); - $(this).toggleClass('btn-danger btn-info'); - return false; - }).end().sortable({ - items: '.sortables' - }); - }); - content.find('[data-multiple-fileupload]').change(function() { - var ext, file, i, image_container, input, len, ref, results; - input = this; - $("#" + input.id).parent().children(".preview").remove(); - ref = input.files; - results = []; - for (i = 0, len = ref.length; i < len; i++) { - file = ref[i]; - ext = file.name.split('.').pop().toLowerCase(); - if ($.inArray(ext, ['gif', 'png', 'jpg', 'jpeg', 'bmp']) === -1) { - continue; - } - image_container = $('').addClass('preview').addClass('img-thumbnail'); - results.push((function(image_container) { - var reader; - reader = new FileReader(); - reader.onload = function(e) { - image_container.attr("src", e.target.result); - }; - reader.readAsDataURL(file); - return $("#" + input.id).parent().append($('
    ').addClass('preview').append(image_container)); - })(image_container)); - } - return results; - }); - content.find('[data-filteringmultiselect]').each(function() { - $(this).filteringMultiselect($(this).data('options')); - if ($(this).parents("#modal").length) { - $(this).siblings('.btn').remove(); - } else { - $(this).parents('.control-group').first().remoteForm(); - } - }); - content.find('[data-filteringselect]').each(function() { - $(this).filteringSelect($(this).data('options')); - if ($(this).parents("#modal").length) { - $(this).siblings('.btn').remove(); - } else { - $(this).parents('.control-group').first().remoteForm(); - } - }); - content.find('[data-nestedmany]').each(function() { - var field, nav, tab_content, toggler; - field = $(this).parents('.control-group').first(); - nav = field.find('> .controls > .nav'); - tab_content = field.find('> .tab-content'); - toggler = field.find('> .controls > .btn-group > .toggler'); - tab_content.children('.fields:not(.tab-pane)').addClass('tab-pane').each(function() { - $(this).attr('id', 'unique-id-' + (new Date().getTime()) + Math.floor(Math.random() * 100000)); - nav.append( - $('
  • ').append( - $('').attr('data-toggle', 'tab').attr('href', '#' + this.id).text($(this).children('.object-infos').data('object-label')) - ) - ); - }); - if (nav.find("> li.active").length === 0) { - nav.find("> li > a[data-toggle='tab']:first").tab('show'); - } - if (nav.children().length === 0) { - nav.hide(); - tab_content.hide(); - toggler.addClass('disabled').removeClass('active').children('i').addClass('icon-chevron-right'); - } else { - if (toggler.hasClass('active')) { - nav.show(); - tab_content.show(); - toggler.children('i').addClass('icon-chevron-down'); - } else { - nav.hide(); - tab_content.hide(); - toggler.children('i').addClass('icon-chevron-right'); - } - } - }); - content.find('[data-nestedone]').each(function() { - var field, first_tab, nav, tab_content, toggler; - field = $(this).parents('.control-group').first(); - nav = field.find("> .controls > .nav"); - tab_content = field.find("> .tab-content"); - toggler = field.find('> .controls > .btn-group > .toggler'); - tab_content.children(".fields:not(.tab-pane)").addClass('tab-pane active').each(function() { - field.find('> .controls .add_nested_fields').removeClass('add_nested_fields').text($(this).children('.object-infos').data('object-label')); - nav.append( - $('
  • ').append( - $('').attr('data-toggle', 'tab').attr('href', '#' + this.id).text($(this).children('.object-infos').data('object-label')) - ) - ); - }); - first_tab = nav.find("> li > a[data-toggle='tab']:first"); - first_tab.tab('show'); - field.find("> .controls > [data-target]:first").html(' ' + first_tab.html()); - nav.hide(); - if (nav.children().length === 0) { - nav.hide(); - tab_content.hide(); - toggler.addClass('disabled').removeClass('active').children('i').addClass('icon-chevron-right'); - } else { - if (toggler.hasClass('active')) { - toggler.children('i').addClass('icon-chevron-down'); - tab_content.show(); - } else { - toggler.children('i').addClass('icon-chevron-right'); - tab_content.hide(); - } - } - }); - content.find('[data-polymorphic]').each(function() { - var field, object_select, type_select, urls; - type_select = $(this); - field = type_select.parents('.control-group').first(); - object_select = field.find('select').last(); - urls = type_select.data('urls'); - type_select.on('change', function(e) { - var selected_data, selected_type; - selected_type = type_select.val().toLowerCase(); - selected_data = $("#" + selected_type + "-js-options").data('options'); - object_select.data('options', selected_data); - object_select.filteringSelect("destroy"); - object_select.filteringSelect(selected_data); - }); - }); - goSimpleMDEs = function() { - return content.find('[data-richtext=simplemde]').not('.simplemded').each(function(index, domEle) { - var instance_config, options; - options = $(this).data('options'); - instance_config = options.instance_config; - new window.SimpleMDE($.extend(true, { - element: document.getElementById(this.id), - autosave: { - uniqueId: this.id - } - }, instance_config)); - $(this).addClass('simplemded'); - }); - }; - $editors = content.find('[data-richtext=simplemde]').not('.simplemded'); - if ($editors.length) { - if (!window.SimpleMDE) { - options = $editors.first().data('options'); - $('head').append(''); - $.getScript(options['js_location'], function(script, textStatus, jqXHR) { - return goSimpleMDEs(); - }); - } else { - goSimpleMDEs(); - } - } - goCkeditors = function() { - return content.find('[data-richtext=ckeditor]').not('.ckeditored').each(function(index, domEle) { - var instance; - try { - if (instance = window.CKEDITOR.instances[this.id]) { - instance.destroy(true); - } - } catch (error1) {} - window.CKEDITOR.replace(this, $(this).data('options').options); - $(this).addClass('ckeditored'); - }); - }; - $editors = content.find('[data-richtext=ckeditor]').not('.ckeditored'); - if ($editors.length) { - if (!window.CKEDITOR) { - options = $editors.first().data('options'); - window.CKEDITOR_BASEPATH = options['base_location']; - $.getScript(options['jspath'], (function(_this) { - return function(script, textStatus, jqXHR) { - return goCkeditors(); - }; - })(this)); - } else { - goCkeditors(); - } - } - goCodeMirrors = (function(_this) { - return function(array) { - return array.each(function(index, domEle) { - var textarea; - options = $(this).data('options'); - textarea = this; - $.getScript(options['locations']['mode'], function(script, textStatus, jqXHR) { - options = $(domEle).data('options'); - $('head').append(''); - CodeMirror.fromTextArea(textarea, options['options']); - return $(textarea).addClass('codemirrored'); - }); - }); - }; - })(this); - array = content.find('[data-richtext=codemirror]').not('.codemirrored'); - if (array.length) { - this.array = array; - if (!window.CodeMirror) { - options = $(array[0]).data('options'); - $('head').append(''); - $.getScript(options['jspath'], (function(_this) { - return function(script, textStatus, jqXHR) { - return goCodeMirrors(_this.array); - }; - })(this)); - } else { - goCodeMirrors(this.array); - } - } - goBootstrapWysihtml5s = (function(_this) { - return function(array, config_options) { - return array.each(function() { - $(this).addClass('bootstrap-wysihtml5ed'); - $(this).closest('.controls').addClass('well'); - $(this).wysihtml5(config_options); - }); - }; - })(this); - array = content.find('[data-richtext=bootstrap-wysihtml5]').not('.bootstrap-wysihtml5ed'); - if (array.length) { - this.array = array; - options = $(array[0]).data('options'); - config_options = $.parseJSON(options['config_options']); - if (!window.wysihtml5) { - $('head').append(''); - $.getScript(options['jspath'], (function(_this) { - return function(script, textStatus, jqXHR) { - return goBootstrapWysihtml5s(_this.array, config_options); - }; - })(this)); - } else { - goBootstrapWysihtml5s(this.array, config_options); - } - } - goFroalaWysiwygs = (function(_this) { - return function(array) { - return array.each(function() { - var uploadEnabled; - options = $(this).data('options'); - config_options = $.parseJSON(options['config_options']); - if (config_options) { - if (!config_options['inlineMode']) { - config_options['inlineMode'] = false; - } - } else { - config_options = { - inlineMode: false - }; - } - uploadEnabled = config_options['imageUploadURL'] ? config_options['imageUploadParams'] = { - authenticity_token: $('meta[name=csrf-token]').attr('content') - } : void 0; - $(this).addClass('froala-wysiwyged'); - $(this).froalaEditor(config_options); - if (uploadEnabled) { - $(this).on('froalaEditor.image.error', function(e, editor, error) { - alert("error uploading image: " + error.message); - }).on('froalaEditor.image.removed', function(e, editor, $img) { - editor.options.imageDeleteParams = { - src: $img.attr('src'), - authenticity_token: $('meta[name=csrf-token]').attr('content') - }; - editor.deleteImage($img); - }).on('editable.imageDeleteSuccess', function(e, editor, data) {}).on('editable.imageDeleteError', function(e, editor, error) { - alert("error deleting image: " + error.message); - }); - } - }); - }; - })(this); - array = content.find('[data-richtext=froala-wysiwyg]').not('.froala-wysiwyged'); - if (array.length) { - options = $(array[0]).data('options'); - if (!$.isFunction($.fn.editable)) { - $('head').append(''); - $.getScript(options['jspath'], (function(_this) { - return function(script, textStatus, jqXHR) { - return goFroalaWysiwygs(array); - }; - })(this)); - } else { - goFroalaWysiwygs(array); - } - } - return content.find('trix-editor').each(function() { - if (!window.Trix) { - options = $(this).data('options'); - $('head').append(''); - return $.getScript(options['jspath']); - } - }); - } - }); - -}(jQuery)); diff --git a/app/assets/javascripts/rails_admin/rails_admin.js b/app/assets/javascripts/rails_admin/rails_admin.js deleted file mode 100644 index 8b0abefe72..0000000000 --- a/app/assets/javascripts/rails_admin/rails_admin.js +++ /dev/null @@ -1,20 +0,0 @@ -//= require 'jquery2' -//= require 'jquery_ujs' -//= require 'jquery.remotipart' -//= require 'rails_admin/jquery-ui' -//= require 'rails_admin/moment-with-locales' -//= require 'rails_admin/bootstrap-datetimepicker' -//= require 'rails_admin/jquery.colorpicker' -//= require 'rails_admin/ra.filter-box' -//= require 'rails_admin/ra.filtering-multiselect' -//= require 'rails_admin/ra.filtering-select' -//= require 'rails_admin/ra.remote-form' -//= require 'rails_admin/jquery.pjax' -//= require 'jquery_nested_form' -//= require 'rails_admin/ra.nested-form-hooks' -//= require 'rails_admin/ra.i18n' -//= require 'rails_admin/bootstrap/bootstrap' -//= require 'rails_admin/ra.widgets' -//= require 'rails_admin/ra.sidescroll' -//= require 'rails_admin/ui' -//= require 'rails_admin/custom/ui' diff --git a/app/assets/javascripts/rails_admin/ui.js b/app/assets/javascripts/rails_admin/ui.js deleted file mode 100644 index f12fb77a13..0000000000 --- a/app/assets/javascripts/rails_admin/ui.js +++ /dev/null @@ -1,135 +0,0 @@ -(function($) { - $(document).on("click", "#list input.toggle", function() { - $("#list [name='bulk_ids[]']").prop("checked", $(this).is(":checked")); - }); - - $(document).on('click', '.pjax', function(event) { - if (event.which > 1 || event.metaKey || event.ctrlKey) { - return; - } - - if ($.support.pjax) { - event.preventDefault(); - $.pjax({ - container: $(this).data('pjax-container') || '[data-pjax-container]', - url: $(this).data('href') || $(this).attr('href'), - timeout: 2000 - }); - return; - } - - if ($(this).data('href')) { - window.location = $(this).data('href'); - } - }); - - $(document).on('submit', '.pjax-form', function(event) { - if ($.support.pjax) { - event.preventDefault(); - $.pjax({ - container: $(this).data('pjax-container') || '[data-pjax-container]', - url: this.action + (this.action.indexOf('?') !== -1 ? '&' : '?') + $(this).serialize(), - timeout: 2000 - }); - } - }); - - $(document).on('pjax:start', function() { - return $('#loading').show(); - }).on('pjax:end', function() { - return $('#loading').hide(); - }); - - $(document).on('click', '[data-target]', function() { - if (!$(this).hasClass('disabled')) { - if ($(this).has('i.icon-chevron-down').length) { - $(this).removeClass('active').children('i').toggleClass('icon-chevron-down icon-chevron-right'); - $($(this).data('target')).select(':visible').hide('slow'); - } else { - if ($(this).has('i.icon-chevron-right').length) { - $(this).addClass('active').children('i').toggleClass('icon-chevron-down icon-chevron-right'); - $($(this).data('target')).select(':hidden').show('slow'); - } - } - } - }); - - $(document).on('click', '.form-horizontal legend', function() { - if ($(this).has('i.icon-chevron-down').length) { - $(this).siblings('.control-group:visible').hide('slow'); - $(this).children('i').toggleClass('icon-chevron-down icon-chevron-right'); - } else { - if ($(this).has('i.icon-chevron-right').length) { - $(this).siblings('.control-group:hidden').show('slow'); - $(this).children('i').toggleClass('icon-chevron-down icon-chevron-right'); - } - } - }); - - $(document).on('click', 'form .tab-content .tab-pane a.remove_nested_one_fields', function() { - $(this).children('input[type="hidden"]').val($(this).hasClass('active')).siblings('i').toggleClass('icon-check icon-trash'); - }); - - $(document).ready(function() { - RailsAdmin.I18n.init($('html').attr('lang'), $("#admin-js").data('i18nOptions')); - $(document).trigger('rails_admin.dom_ready'); - }); - - $(document).on('pjax:end', function() { - $(document).trigger('rails_admin.dom_ready'); - }); - - $(document).on('rails_admin.dom_ready', function() { - $('.nav.nav-pills li.active').removeClass('active'); - $(".nav.nav-pills li[data-model=\"" + ($('.page-header').data('model')) + "\"]").addClass('active'); - $('.animate-width-to').each(function() { - var length, width; - length = $(this).data("animate-length"); - width = $(this).data("animate-width-to"); - $(this).animate({ - width: width - }, length, 'easeOutQuad'); - }); - $('.form-horizontal legend').has('i.icon-chevron-right').each(function() { - $(this).siblings('.control-group').hide(); - }); - $(".table").tooltip({ - selector: "th[rel=tooltip]" - }); - $('[formnovalidate]').on('click', function() { - $(this).closest('form').attr('novalidate', true); - }); - $.each($('#filters_box').data('options'), function() { - $.filters.append(this); - }); - }); - - $(document).on('click', ".bulk-link", function(event) { - event.preventDefault(); - $('#bulk_action').val($(this).data('action')); - $('#bulk_form').submit(); - }); - - $(document).on('click', "#remove_filter", function(event) { - event.preventDefault(); - $("#filters_box").html(""); - $("hr.filters_box").hide(); - $(this).parent().siblings("input[type='search']").val(""); - $(this).parents("form").submit(); - }); - - $(document).on('click', '#fields_to_export label input#check_all', function() { - var elems; - elems = $('#fields_to_export label input'); - if ($('#fields_to_export label input#check_all').is(':checked')) { - $(elems).prop('checked', true); - } else { - $(elems).prop('checked', false); - } - }); - - $(document).on('click', '#fields_to_export .reverse-selection', function() { - $(this).closest(".control-group").find(".controls").find("input").click(); - }); - -}(jQuery)); diff --git a/app/assets/stylesheets/rails_admin/application.scss.erb b/app/assets/stylesheets/rails_admin/application.scss.erb new file mode 100644 index 0000000000..133163db2b --- /dev/null +++ b/app/assets/stylesheets/rails_admin/application.scss.erb @@ -0,0 +1,35 @@ +@charset "UTF-8"; + +/*** Variables ***/ + +@import "rails_admin/custom/variables"; +@import "rails_admin/styles/base/variables"; + +/*** Mixins ***/ + +@import "rails_admin/styles/base/mixins"; +@import "rails_admin/custom/mixins"; + +/*** Bootstrap ***/ + +@import "rails_admin/bootstrap/bootstrap"; + +/*** Libraries ***/ + +@import "rails_admin/flatpickr"; +@import "rails_admin/styles/filtering-select"; +@import "rails_admin/styles/filtering-multiselect"; +@import "rails_admin/styles/widgets"; + +/*** Font-awesome ***/ + +@import "rails_admin/font-awesome"; + +/*** RailsAdmin Theming ***/ + +@import "rails_admin/styles/base/theming"; +@import "rails_admin/custom/theming"; + +<% if defined?(ActionText::Engine) && Rails.gem_version >= Gem::Version.new('7.0') %> +@import "trix"; +<% end %> diff --git a/app/assets/stylesheets/rails_admin/aristo/images/bg_fallback.png b/app/assets/stylesheets/rails_admin/aristo/images/bg_fallback.png deleted file mode 100644 index 155ebd7e93..0000000000 Binary files a/app/assets/stylesheets/rails_admin/aristo/images/bg_fallback.png and /dev/null differ diff --git a/app/assets/stylesheets/rails_admin/aristo/images/icon_sprite.png b/app/assets/stylesheets/rails_admin/aristo/images/icon_sprite.png deleted file mode 100644 index 0b376bf66c..0000000000 Binary files a/app/assets/stylesheets/rails_admin/aristo/images/icon_sprite.png and /dev/null differ diff --git a/app/assets/stylesheets/rails_admin/aristo/images/progress_bar.gif b/app/assets/stylesheets/rails_admin/aristo/images/progress_bar.gif deleted file mode 100644 index c3d43fa40b..0000000000 Binary files a/app/assets/stylesheets/rails_admin/aristo/images/progress_bar.gif and /dev/null differ diff --git a/app/assets/stylesheets/rails_admin/aristo/images/slider_handles.png b/app/assets/stylesheets/rails_admin/aristo/images/slider_handles.png deleted file mode 100644 index 872eaac305..0000000000 Binary files a/app/assets/stylesheets/rails_admin/aristo/images/slider_handles.png and /dev/null differ diff --git a/app/assets/stylesheets/rails_admin/aristo/images/ui-icons_222222_256x240.png b/app/assets/stylesheets/rails_admin/aristo/images/ui-icons_222222_256x240.png deleted file mode 100644 index 664494d838..0000000000 Binary files a/app/assets/stylesheets/rails_admin/aristo/images/ui-icons_222222_256x240.png and /dev/null differ diff --git a/app/assets/stylesheets/rails_admin/aristo/images/ui-icons_454545_256x240.png b/app/assets/stylesheets/rails_admin/aristo/images/ui-icons_454545_256x240.png deleted file mode 100644 index 8cccf85760..0000000000 Binary files a/app/assets/stylesheets/rails_admin/aristo/images/ui-icons_454545_256x240.png and /dev/null differ diff --git a/app/assets/stylesheets/rails_admin/aristo/jquery-ui-1.8.7.custom.scss b/app/assets/stylesheets/rails_admin/aristo/jquery-ui-1.8.7.custom.scss deleted file mode 100644 index 7c539a0529..0000000000 --- a/app/assets/stylesheets/rails_admin/aristo/jquery-ui-1.8.7.custom.scss +++ /dev/null @@ -1,733 +0,0 @@ -/* - * jQuery UI CSS Framework 1.8.7 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Theming/API - */ - -/* Layout helpers -----------------------------------*/ -.ui-helper-hidden { display: none; } -.ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); } -.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; } -.ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } -.ui-helper-clearfix { display: inline-block; } -/* required comment for clearfix to work in Opera \*/ -* html .ui-helper-clearfix { height:1%; } -.ui-helper-clearfix { display:block; } -/* end clearfix */ -.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); } - - -/* Interaction Cues -----------------------------------*/ -.ui-state-disabled { cursor: default !important; } - - -/* Icons -----------------------------------*/ - -/* states and images */ -.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } - - -/* Misc visuals -----------------------------------*/ - -/* Overlays */ -.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } - - -/* - * jQuery UI CSS Framework 1.8.7 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Theming/API - * - * To view and modify this theme, visit http://jqueryui.com/themeroller/?ctl=themeroller - */ - - -/* Component containers -----------------------------------*/ -.ui-widget { font-family: Arial,sans-serif; font-size: 1.1em; } -.ui-widget .ui-widget { font-size: 1em; } -.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Arial,sans-serif; font-size: 1em; } -.ui-widget-content { border: 1px solid #B6B6B6; background: #ffffff; color: #4F4F4F; } -.ui-widget-content a { color: #4F4F4F; } -.ui-widget-header { border: 1px solid #B6B6B6; color: #4F4F4F; font-weight: bold; } -.ui-widget-header { - background: #ededed image-url('rails_admin/aristo/images/bg_fallback.png') 0 0 repeat-x; /* Old browsers */ - background: -moz-linear-gradient(top, #ededed 0%, #c4c4c4 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ededed), color-stop(100%,#c4c4c4)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #ededed 0%,#c4c4c4 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #ededed 0%,#c4c4c4 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, #ededed 0%,#c4c4c4 100%); /* IE10+ */ - background: linear-gradient(to bottom, #ededed 0%,#c4c4c4 100%); /* W3C */ -} -.ui-widget-header a { color: #4F4F4F; } - -/* Interaction states -----------------------------------*/ -.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #B6B6B6; font-weight: normal; color: #4F4F4F; } -.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { - background: #ededed image-url('rails_admin/aristo/images/bg_fallback.png') 0 0 repeat-x; /* Old browsers */ - background: -moz-linear-gradient(top, #ededed 0%, #c4c4c4 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ededed), color-stop(100%,#c4c4c4)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #ededed 0%,#c4c4c4 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #ededed 0%,#c4c4c4 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, #ededed 0%,#c4c4c4 100%); /* IE10+ */ - background: linear-gradient(to bottom, #ededed 0%,#c4c4c4 100%); /* W3C */ - -webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset; - -moz-box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset; - box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset; -} -.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #4F4F4F; text-decoration: none; } -.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { background-color: #b3e2f0; font-weight: normal; color: #313131; } -.ui-state-hover a, .ui-state-hover a:hover { color: #313131; text-decoration: none; } -.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { - outline: none; - color: #1c4257; border: 1px solid #7096ab; - background: #ededed image-url('rails_admin/aristo/images/bg_fallback.png') 0 -50px repeat-x; /* Old browsers */ - background: -moz-linear-gradient(top, #b9e0f5 0%, #92bdd6 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b9e0f5), color-stop(100%,#92bdd6)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #b9e0f5 0%,#92bdd6 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #b9e0f5 0%,#92bdd6 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, #b9e0f5 0%,#92bdd6 100%); /* IE10+ */ - background: linear-gradient(to bottom, #b9e0f5 0%,#92bdd6 100%); /* W3C */ - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} -.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #313131; text-decoration: none; } -.ui-widget :active { outline: none; } - -/* Interaction Cues -----------------------------------*/ -.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight { border: 1px solid #d2dbf4; background: #f4f8fd; color: #0d2054; -moz-border-radius: 0 !important; -webkit-border-radius: 0 !important; border-radius: 0 !important; } -.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; } -.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error { border: 1px solid #e2d0d0; background: #fcf0f0; color: #280b0b; -moz-border-radius: 0 !important; -webkit-border-radius: 0 !important; border-radius: 0 !important; } -.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a; } -.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a; } -.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; } -.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; } -.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } - -/* Icons -----------------------------------*/ - -/* states and images */ -.ui-icon { width: 16px; height: 16px; background-image: image-url('rails_admin/aristo/images/ui-icons_222222_256x240.png'); } -.ui-widget-content .ui-icon {background-image: image-url('rails_admin/aristo/images/ui-icons_222222_256x240.png'); } -.ui-widget-header .ui-icon {background-image: image-url('rails_admin/aristo/images/ui-icons_222222_256x240.png'); } -.ui-state-default .ui-icon { background-image: image-url('rails_admin/aristo/images/ui-icons_454545_256x240.png'); } -.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: image-url('rails_admin/aristo/images/ui-icons_454545_256x240.png'); } -.ui-state-active .ui-icon {background-image: image-url('rails_admin/aristo/images/ui-icons_454545_256x240.png'); } -.ui-state-highlight .ui-icon {background-image: image-url('rails_admin/aristo/images/ui-icons_454545_256x240.png'); } -.ui-state-error .ui-icon, .ui-state-error-text .ui-icon { background: image-url('rails_admin/aristo/images/icon_sprite.png') -16px 0 no-repeat !important; } -.ui-state-highlight .ui-icon, .ui-state-error .ui-icon { margin-top: -1px; } - -/* positioning */ -.ui-icon-carat-1-n { background-position: 0 0; } -.ui-icon-carat-1-ne { background-position: -16px 0; } -.ui-icon-carat-1-e { background-position: -32px 0; } -.ui-icon-carat-1-se { background-position: -48px 0; } -.ui-icon-carat-1-s { background-position: -64px 0; } -.ui-icon-carat-1-sw { background-position: -80px 0; } -.ui-icon-carat-1-w { background-position: -96px 0; } -.ui-icon-carat-1-nw { background-position: -112px 0; } -.ui-icon-carat-2-n-s { background-position: -128px 0; } -.ui-icon-carat-2-e-w { background-position: -144px 0; } -.ui-icon-triangle-1-n { background-position: 0 -16px; } -.ui-icon-triangle-1-ne { background-position: -16px -16px; } -.ui-icon-triangle-1-e { background-position: -32px -16px; } -.ui-icon-triangle-1-se { background-position: -48px -16px; } -.ui-icon-triangle-1-s { background-position: -64px -16px; } -.ui-icon-triangle-1-sw { background-position: -80px -16px; } -.ui-icon-triangle-1-w { background-position: -96px -16px; } -.ui-icon-triangle-1-nw { background-position: -112px -16px; } -.ui-icon-triangle-2-n-s { background-position: -128px -16px; } -.ui-icon-triangle-2-e-w { background-position: -144px -16px; } -.ui-icon-arrow-1-n { background-position: 0 -32px; } -.ui-icon-arrow-1-ne { background-position: -16px -32px; } -.ui-icon-arrow-1-e { background-position: -32px -32px; } -.ui-icon-arrow-1-se { background-position: -48px -32px; } -.ui-icon-arrow-1-s { background-position: -64px -32px; } -.ui-icon-arrow-1-sw { background-position: -80px -32px; } -.ui-icon-arrow-1-w { background-position: -96px -32px; } -.ui-icon-arrow-1-nw { background-position: -112px -32px; } -.ui-icon-arrow-2-n-s { background-position: -128px -32px; } -.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } -.ui-icon-arrow-2-e-w { background-position: -160px -32px; } -.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } -.ui-icon-arrowstop-1-n { background-position: -192px -32px; } -.ui-icon-arrowstop-1-e { background-position: -208px -32px; } -.ui-icon-arrowstop-1-s { background-position: -224px -32px; } -.ui-icon-arrowstop-1-w { background-position: -240px -32px; } -.ui-icon-arrowthick-1-n { background-position: 0 -48px; } -.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } -.ui-icon-arrowthick-1-e { background-position: -32px -48px; } -.ui-icon-arrowthick-1-se { background-position: -48px -48px; } -.ui-icon-arrowthick-1-s { background-position: -64px -48px; } -.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } -.ui-icon-arrowthick-1-w { background-position: -96px -48px; } -.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } -.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } -.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } -.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } -.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } -.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } -.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } -.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } -.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } -.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } -.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } -.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } -.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } -.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } -.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } -.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } -.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } -.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } -.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } -.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } -.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } -.ui-icon-arrow-4 { background-position: 0 -80px; } -.ui-icon-arrow-4-diag { background-position: -16px -80px; } -.ui-icon-extlink { background-position: -32px -80px; } -.ui-icon-newwin { background-position: -48px -80px; } -.ui-icon-refresh { background-position: -64px -80px; } -.ui-icon-shuffle { background-position: -80px -80px; } -.ui-icon-transfer-e-w { background-position: -96px -80px; } -.ui-icon-transferthick-e-w { background-position: -112px -80px; } -.ui-icon-folder-collapsed { background-position: 0 -96px; } -.ui-icon-folder-open { background-position: -16px -96px; } -.ui-icon-document { background-position: -32px -96px; } -.ui-icon-document-b { background-position: -48px -96px; } -.ui-icon-note { background-position: -64px -96px; } -.ui-icon-mail-closed { background-position: -80px -96px; } -.ui-icon-mail-open { background-position: -96px -96px; } -.ui-icon-suitcase { background-position: -112px -96px; } -.ui-icon-comment { background-position: -128px -96px; } -.ui-icon-person { background-position: -144px -96px; } -.ui-icon-print { background-position: -160px -96px; } -.ui-icon-trash { background-position: -176px -96px; } -.ui-icon-locked { background-position: -192px -96px; } -.ui-icon-unlocked { background-position: -208px -96px; } -.ui-icon-bookmark { background-position: -224px -96px; } -.ui-icon-tag { background-position: -240px -96px; } -.ui-icon-home { background-position: 0 -112px; } -.ui-icon-flag { background-position: -16px -112px; } -.ui-icon-calendar { background-position: -32px -112px; } -.ui-icon-cart { background-position: -48px -112px; } -.ui-icon-pencil { background-position: -64px -112px; } -.ui-icon-clock { background-position: -80px -112px; } -.ui-icon-disk { background-position: -96px -112px; } -.ui-icon-calculator { background-position: -112px -112px; } -.ui-icon-zoomin { background-position: -128px -112px; } -.ui-icon-zoomout { background-position: -144px -112px; } -.ui-icon-search { background-position: -160px -112px; } -.ui-icon-wrench { background-position: -176px -112px; } -.ui-icon-gear { background-position: -192px -112px; } -.ui-icon-heart { background-position: -208px -112px; } -.ui-icon-star { background-position: -224px -112px; } -.ui-icon-link { background-position: -240px -112px; } -.ui-icon-cancel { background-position: 0 -128px; } -.ui-icon-plus { background-position: -16px -128px; } -.ui-icon-plusthick { background-position: -32px -128px; } -.ui-icon-minus { background-position: -48px -128px; } -.ui-icon-minusthick { background-position: -64px -128px; } -.ui-icon-close { background-position: -80px -128px; } -.ui-icon-closethick { background-position: -96px -128px; } -.ui-icon-key { background-position: -112px -128px; } -.ui-icon-lightbulb { background-position: -128px -128px; } -.ui-icon-scissors { background-position: -144px -128px; } -.ui-icon-clipboard { background-position: -160px -128px; } -.ui-icon-copy { background-position: -176px -128px; } -.ui-icon-contact { background-position: -192px -128px; } -.ui-icon-image { background-position: -208px -128px; } -.ui-icon-video { background-position: -224px -128px; } -.ui-icon-script { background-position: -240px -128px; } -.ui-icon-alert { background-position: 0 -144px; } -.ui-icon-info { background: image-url('rails_admin/aristo/images/icon_sprite.png') 0 0 no-repeat !important; } -.ui-icon-notice { background-position: -32px -144px; } -.ui-icon-help { background-position: -48px -144px; } -.ui-icon-check { background-position: -64px -144px; } -.ui-icon-bullet { background-position: -80px -144px; } -.ui-icon-radio-off { background-position: -96px -144px; } -.ui-icon-radio-on { background-position: -112px -144px; } -.ui-icon-pin-w { background-position: -128px -144px; } -.ui-icon-pin-s { background-position: -144px -144px; } -.ui-icon-play { background-position: 0 -160px; } -.ui-icon-pause { background-position: -16px -160px; } -.ui-icon-seek-next { background-position: -32px -160px; } -.ui-icon-seek-prev { background-position: -48px -160px; } -.ui-icon-seek-end { background-position: -64px -160px; } -.ui-icon-seek-start { background-position: -80px -160px; } -/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ -.ui-icon-seek-first { background-position: -80px -160px; } -.ui-icon-stop { background-position: -96px -160px; } -.ui-icon-eject { background-position: -112px -160px; } -.ui-icon-volume-off { background-position: -128px -160px; } -.ui-icon-volume-on { background-position: -144px -160px; } -.ui-icon-power { background-position: 0 -176px; } -.ui-icon-signal-diag { background-position: -16px -176px; } -.ui-icon-signal { background-position: -32px -176px; } -.ui-icon-battery-0 { background-position: -48px -176px; } -.ui-icon-battery-1 { background-position: -64px -176px; } -.ui-icon-battery-2 { background-position: -80px -176px; } -.ui-icon-battery-3 { background-position: -96px -176px; } -.ui-icon-circle-plus { background-position: 0 -192px; } -.ui-icon-circle-minus { background-position: -16px -192px; } -.ui-icon-circle-close { background-position: -32px -192px; } -.ui-icon-circle-triangle-e { background-position: -48px -192px; } -.ui-icon-circle-triangle-s { background-position: -64px -192px; } -.ui-icon-circle-triangle-w { background-position: -80px -192px; } -.ui-icon-circle-triangle-n { background-position: -96px -192px; } -.ui-icon-circle-arrow-e { background-position: -112px -192px; } -.ui-icon-circle-arrow-s { background-position: -128px -192px; } -.ui-icon-circle-arrow-w { background-position: -144px -192px; } -.ui-icon-circle-arrow-n { background-position: -160px -192px; } -.ui-icon-circle-zoomin { background-position: -176px -192px; } -.ui-icon-circle-zoomout { background-position: -192px -192px; } -.ui-icon-circle-check { background-position: -208px -192px; } -.ui-icon-circlesmall-plus { background-position: 0 -208px; } -.ui-icon-circlesmall-minus { background-position: -16px -208px; } -.ui-icon-circlesmall-close { background-position: -32px -208px; } -.ui-icon-squaresmall-plus { background-position: -48px -208px; } -.ui-icon-squaresmall-minus { background-position: -64px -208px; } -.ui-icon-squaresmall-close { background-position: -80px -208px; } -.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } -.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } -.ui-icon-grip-solid-vertical { background-position: -32px -224px; } -.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } -.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } -.ui-icon-grip-diagonal-se { background-position: -80px -224px; } - - -/* Misc visuals -----------------------------------*/ - -/* Corner radius */ -.ui-corner-tl { -moz-border-radius-topleft: 3px; -webkit-border-top-left-radius: 3px; border-top-left-radius: 3px; } -.ui-corner-tr { -moz-border-radius-topright: 3px; -webkit-border-top-right-radius: 3px; border-top-right-radius: 3px; } -.ui-corner-bl { -moz-border-radius-bottomleft: 3px; -webkit-border-bottom-left-radius: 3px; border-bottom-left-radius: 3px; } -.ui-corner-br { -moz-border-radius-bottomright: 3px; -webkit-border-bottom-right-radius: 3px; border-bottom-right-radius: 3px; } -.ui-corner-top { -moz-border-radius-topleft: 3px; -webkit-border-top-left-radius: 3px; border-top-left-radius: 3px; -moz-border-radius-topright: 3px; -webkit-border-top-right-radius: 3px; border-top-right-radius: 3px; } -.ui-corner-bottom { -moz-border-radius-bottomleft: 3px; -webkit-border-bottom-left-radius: 3px; border-bottom-left-radius: 3px; -moz-border-radius-bottomright: 3px; -webkit-border-bottom-right-radius: 3px; border-bottom-right-radius: 3px; } -.ui-corner-right { -moz-border-radius-topright: 3px; -webkit-border-top-right-radius: 3px; border-top-right-radius: 3px; -moz-border-radius-bottomright: 3px; -webkit-border-bottom-right-radius: 3px; border-bottom-right-radius: 3px; } -.ui-corner-left { -moz-border-radius-topleft: 3px; -webkit-border-top-left-radius: 3px; border-top-left-radius: 3px; -moz-border-radius-bottomleft: 3px; -webkit-border-bottom-left-radius: 3px; border-bottom-left-radius: 3px; } -.ui-corner-all { -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; } - -/* Overlays */ -.ui-widget-overlay { background: #262b33; opacity: .70;filter:Alpha(Opacity=70); } -.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #000000; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }/* - * jQuery UI Resizable 1.8.7 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Resizable#theming - */ -.ui-resizable { position: relative;} -.ui-resizable-handle { position: absolute; font-size: 0.1px; z-index: 999; display: block;} -.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; } -.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; } -.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; } -.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; } -.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; } -.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; } -.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; } -.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; } -.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/* - * jQuery UI Selectable 1.8.7 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Selectable#theming - */ -.ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; } -/* - * jQuery UI Accordion 1.8.7 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Accordion#theming - */ -/* IE/Win - Fix animation bug - #4615 */ -.ui-accordion { width: 100%; } -.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; } -.ui-accordion .ui-accordion-header, .ui-accordion .ui-accordion-content { -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; } -.ui-accordion .ui-accordion-li-fix { display: inline; } -.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; } -.ui-accordion .ui-accordion-header a { display: block; font-size: 12px; font-weight: bold; padding: .5em .5em .5em .7em; } -.ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; } -.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; } -.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; } -.ui-accordion .ui-accordion-content-active { display: block; }/* - * jQuery UI Autocomplete 1.8.7 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Autocomplete#theming - */ -.ui-autocomplete { - position: absolute; cursor: default; z-index: 1051 !important; - -moz-border-radius: 0; - -webkit-border-radius: 0; - border-radius: 0; - -moz-box-shadow: 0 1px 5px rgba(0,0,0,0.3); - -webkit-box-shadow: 0 1px 5px rgba(0,0,0,0.3); - box-shadow: 0 1px 5px rgba(0,0,0,0.3); -} - -/* workarounds */ -* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */ - -/* - * jQuery UI Menu 1.8.7 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Menu#theming - */ -.ui-menu { - list-style:none; - padding: 2px; - margin: 0; - display:block; - float: left; -} -.ui-menu .ui-menu { - margin-top: -3px; -} -.ui-menu .ui-menu-item { - margin:0; - padding: 0; - zoom: 1; - float: left; - clear: left; - width: 100%; -} -.ui-menu .ui-menu-item a { - text-decoration:none; - display:block; - padding:.2em .4em; - line-height:1.5; - zoom:1; -} -.ui-menu .ui-menu-item a.ui-state-hover, -.ui-menu .ui-menu-item a.ui-state-active { - font-weight: normal; - margin: -1px; - background: #5f83b9; - color: #FFFFFF; - text-shadow: 0px 1px 1px #234386; - border-color: #466086; - -moz-border-radius: 0; - -webkit-border-radius: 0; - border-radius: 0; -} -/* - * jQuery UI Button 1.8.7 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Button#theming - */ -.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; -webkit-user-select: none; -moz-user-select: none; user-select: none; } /* the overflow property removes extra width in IE */ -.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */ -button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */ -.ui-button-icons-only { width: 3.4em; } -button.ui-button-icons-only { width: 3.7em; } - -/* button animation properties */ -.ui-button { - -webkit-transition: -webkit-box-shadow 0.25s ease-in-out; - -moz-transition: -moz-box-shadow 0.25s ease-in-out; - -o-transition: -o-box-shadow 0.25s ease-in-out; -} - -/*states*/ -.ui-button.ui-state-hover { - -moz-box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 1px 0 rgba(255,255,255,0.8) inset; - -webkit-box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 1px 0 rgba(255,255,255,0.8) inset; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 1px 0 rgba(255,255,255,0.8) inset; -} -.ui-button.ui-state-focus { - outline: none; - color: #1c4257; - border-color: #7096ab; - background: #ededed image-url('rails_admin/aristo/images/bg_fallback.png') 0 -50px repeat-x; /* Old browsers */ - background: -moz-linear-gradient(top, #b9e0f5 0%, #92bdd6 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b9e0f5), color-stop(100%,#92bdd6)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #b9e0f5 0%,#92bdd6 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #b9e0f5 0%,#92bdd6 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, #b9e0f5 0%,#92bdd6 100%); /* IE10+ */ - background: linear-gradient(to bottom, #b9e0f5 0%,#92bdd6 100%); /* W3C */ - -moz-box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 1px 0 rgba(255,255,255,0.8) inset; - -webkit-box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 1px 0 rgba(255,255,255,0.8) inset; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 1px 0 rgba(255,255,255,0.8) inset; -} - -/*button text element */ -.ui-button .ui-button-text { display: block; line-height: 1.4; font-size: 14px; font-weight: bold; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); } -.ui-button-text-only .ui-button-text { padding: .4em 1em; } -.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; } -.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; } -.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; } -.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; } -/* no icon support for input elements, provide padding by default */ -input.ui-button { font-size: 14px; font-weight: bold; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); padding: 0 1em !important; height: 33px; } -/*remove submit button internal padding in Firefox*/ -input.ui-button::-moz-focus-inner { - border: 0; - padding: 0; -} -/* fix webkits handling of the box model */ -@media screen and (-webkit-min-device-pixel-ratio:0) { - input.ui-button { - height: 31px !important; - } -} - - -/*button icon element(s) */ -.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; } -.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; } -.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; } -.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } -.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } - -/*button sets*/ -.ui-buttonset { margin-right: 7px; } -.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; } -.ui-buttonset .ui-button.ui-state-active { color: #1c4257; border-color: #7096ab; } -.ui-buttonset .ui-button.ui-state-active { - background: #ededed image-url('rails_admin/aristo/images/bg_fallback.png') 0 -50px repeat-x; /* Old browsers */ - background: -moz-linear-gradient(top, #b9e0f5 0%, #92bdd6 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b9e0f5), color-stop(100%,#92bdd6)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #b9e0f5 0%,#92bdd6 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #b9e0f5 0%,#92bdd6 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, #b9e0f5 0%,#92bdd6 100%); /* IE10+ */ - background: linear-gradient(to bottom, #b9e0f5 0%,#92bdd6 100%); /* W3C */ - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} - -/* workarounds */ -button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */ -/* - * jQuery UI Dialog 1.8.7 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Dialog#theming - */ -.ui-dialog { position: absolute; padding: 0; width: 300px; overflow: hidden; } -.ui-dialog { - -webkit-box-shadow: 0 2px 12px rgba(0,0,0,0.6); - -moz-box-shadow: 0 2px 12px rgba(0,0,0,0.6); -} -.ui-dialog .ui-dialog-titlebar { padding: 0.7em 1em 0.6em 1em; position: relative; border: none; border-bottom: 1px solid #979797; -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; } -.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .2em 0; font-size: 14px; text-shadow: 0 1px 0 rgba(255,255,255,0.5); } -.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .8em; top: 55%; width: 16px; margin: -10px 0 0 0; padding: 0; height: 16px; } -.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; background: image-url('rails_admin/aristo/images/icon_sprite.png') 0 -16px no-repeat; } -.ui-dialog .ui-dialog-titlebar-close:hover span { background-position: -16px -16px; } -.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; border: 0; } -.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; } -.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; } -.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; } -.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; } -.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; } -.ui-draggable .ui-dialog-titlebar { cursor: move; } -/* - * jQuery UI Slider 1.8.7 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Slider#theming - */ -.ui-slider { position: relative; text-align: left; background: #d7d7d7; z-index: 1; } -.ui-slider { -moz-box-shadow: 0 1px 2px rgba(0,0,0,0.5) inset; -webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.5) inset; box-shadow: 0 1px 2px rgba(0,0,0,0.5) inset; } -.ui-slider .ui-slider-handle { background: image-url('rails_admin/aristo/images/slider_handles.png') 0px -23px no-repeat; position: absolute; z-index: 2; width: 23px; height: 23px; cursor: default; border: none; outline: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } -.ui-slider .ui-state-hover, .ui-slider .ui-state-active { background-position: 0 0; } -.ui-slider .ui-slider-range { background: #a3cae0; position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; } -.ui-slider .ui-slider-range { -moz-box-shadow: 0 1px 2px rgba(17,35,45,0.6) inset; -webkit-box-shadow: 0 1px 2px rgba(17,35,45,0.6) inset; box-shadow: 0 1px 2px rgba(17,35,45,0.6) inset; } - - -.ui-slider-horizontal { height: 5px; } -.ui-slider-horizontal .ui-slider-handle { top: -8px; margin-left: -13px; } -.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; } -.ui-slider-horizontal .ui-slider-range-min { left: 0; } -.ui-slider-horizontal .ui-slider-range-max { right: 0; } - -.ui-slider-vertical { width: 5px; height: 100px; } -.ui-slider-vertical .ui-slider-handle { left: -8px; margin-left: 0; margin-bottom: -13px; } -.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; } -.ui-slider-vertical .ui-slider-range-min { bottom: 0; } -.ui-slider-vertical .ui-slider-range-max { top: 0; }/* - * jQuery UI Tabs 1.8.7 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Tabs#theming - */ -.ui-tabs { position: relative; zoom: 1; border: 0; background: transparent; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ -.ui-tabs .ui-tabs-nav { margin: 0; padding: 0; background: transparent; border-width: 0 0 1px 0; } -.ui-tabs .ui-tabs-nav { - -moz-border-radius: 0; - -webkit-border-radius: 0; - border-radius: 0; -} -.ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; } -.ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; font-size: 12px; font-weight: bold; text-shadow: 0 1px 0 rgba(255,255,255,0.5); } -.ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; background: #fff; border-color: #B6B6B6; } -.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; outline: none; } -.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ -.ui-tabs .ui-tabs-panel { display: block; border-width: 0 1px 1px 1px; padding: 1em 1.4em; background: none; } -.ui-tabs .ui-tabs-panel { background: #FFF; - -moz-border-radius: 0; - -webkit-border-radius: 0; - border-radius: 0; -} -.ui-tabs .ui-tabs-hide { display: none !important; } -/* - * jQuery UI Datepicker 1.8.7 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Datepicker#theming - */ -.ui-datepicker { width: 17em; padding: 0; display: none; border-color: #DDDDDD; } -.ui-datepicker { - -moz-box-shadow: 0 4px 8px rgba(0,0,0,0.5); - -webkit-box-shadow: 0 4px 8px rgba(0,0,0,0.5); - box-shadow: 0 4px 8px rgba(0,0,0,0.5); -} -.ui-datepicker .ui-datepicker-header { position:relative; padding:.35em 0; border: none; border-bottom: 1px solid #B6B6B6; -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; } -.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 6px; width: 1.8em; height: 1.8em; } -.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { border: 1px none; } -.ui-datepicker .ui-datepicker-prev { left:2px; } -.ui-datepicker .ui-datepicker-next { right:2px; } -.ui-datepicker .ui-datepicker-prev span { background-position: 0px -32px !important; } -.ui-datepicker .ui-datepicker-next span { background-position: -16px -32px !important; } -.ui-datepicker .ui-datepicker-prev-hover span { background-position: 0px -48px !important; } -.ui-datepicker .ui-datepicker-next-hover span { background-position: -16px -48px !important; } -.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; background: image-url('rails_admin/aristo/images/icon_sprite.png') no-repeat; } -.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; font-size: 12px; text-shadow: 0 1px 0 rgba(255,255,255,0.6); } -.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; } -.ui-datepicker select.ui-datepicker-month-year {width: 100%;} -.ui-datepicker select.ui-datepicker-month, -.ui-datepicker select.ui-datepicker-year { width: 49%;} -.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; } -.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; } -.ui-datepicker td { border: 0; padding: 1px; } -.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; } -.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; } -.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } -.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; } -.ui-datepicker-calendar .ui-state-default { background: transparent; border-color: #FFF; } -.ui-datepicker-calendar .ui-state-active { background: #5F83B9; border-color: #5F83B9; color: #FFF; font-weight: bold; text-shadow: 0 1px 1px #234386; } - -/* with multiple calendars */ -.ui-datepicker.ui-datepicker-multi { width:auto; } -.ui-datepicker-multi .ui-datepicker-group { float:left; } -.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; } -.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; } -.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; } -.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; } -.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; } -.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; } -.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; } -.ui-datepicker-row-break { clear:both; width:100%; } - -/* RTL support */ -.ui-datepicker-rtl { direction: rtl; } -.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; } -.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; } -.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; } -.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; } -.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; } -.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; } -.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; } -.ui-datepicker-rtl .ui-datepicker-group { float:right; } -.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; } -.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; } - -/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ -.ui-datepicker-cover { - display: none; /*sorry for IE5*/ - display/**/: block; /*sorry for IE5*/ - position: absolute; /*must have*/ - z-index: -1; /*must have*/ - filter: mask(); /*must have*/ - top: -4px; /*must have*/ - left: -4px; /*must have*/ - width: 200px; /*must have*/ - height: 200px; /*must have*/ -}/* - * jQuery UI Progressbar 1.8.7 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Progressbar#theming - */ -.ui-progressbar { height: 12px; text-align: left; background: #FFF image-url('rails_admin/aristo/images/progress_bar.gif') 0 -14px repeat-x; } -.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; background: image-url('rails_admin/aristo/images/progress_bar.gif') 0 0 repeat-x; } - -/* Extra Input Field Styling */ -.ui-form textarea, .ui-form input:not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]) { - padding: 3px; - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - border-radius: 2px; - border: 1px solid #cecece; - outline: none; - -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.1) inset, 0 1px 0 rgba(255,255,255,0.2); - -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.1) inset, 0 1px 0 rgba(255,255,255,0.2); - box-shadow: 0 1px 3px rgba(0,0,0,0.1) inset, 0 1px 0 rgba(255,255,255,0.2); - -webkit-transition: all 0.25s ease-in-out; - -moz-transition: all 0.25 ease-in-out; - -o-transition: all 0.25s ease-in-out; -} -.ui-form textarea:hover, .ui-form input:not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]):hover { - border: 1px solid #bdbdbd; - -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.2) inset, 0 1px 0 rgba(255,255,255,0.2); - -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.2) inset, 0 1px 0 rgba(255,255,255,0.2); - box-shadow: 0 1px 3px rgba(0,0,0,0.2) inset, 0 1px 0 rgba(255,255,255,0.2); -} -.ui-form textarea:focus, .ui-form input:not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]):focus { - border: 1px solid #95bdd4; - -webkit-box-shadow: 0 2px 3px rgba(161,202,226,0.5) inset, 0 1px 0 rgba(255,255,255,0.2); - -moz-box-shadow: 0 2px 3px rgba(161,202,226,0.5) inset, 0 1px 0 rgba(255,255,255,0.2); - box-shadow: 0 2px 3px rgba(161,202,226,0.5) inset, 0 1px 0 rgba(255,255,255,0.2); -} diff --git a/app/assets/stylesheets/rails_admin/base/font-awesome-4-compability.scss b/app/assets/stylesheets/rails_admin/base/font-awesome-4-compability.scss deleted file mode 100644 index 063e1c7ecb..0000000000 --- a/app/assets/stylesheets/rails_admin/base/font-awesome-4-compability.scss +++ /dev/null @@ -1,153 +0,0 @@ -[class^="icon-"], -[class*=" icon-"] { - @extend .fa !optional; -} - -.icon-glass { @extend .fa-glass !optional; } -.icon-music { @extend .fa-music !optional; } -.icon-search { @extend .fa-search !optional; } -.icon-envelope { @extend .fa-envelope !optional; } -.icon-heart { @extend .fa-heart !optional; } -.icon-star { @extend .fa-star !optional; } -.icon-star-empty { @extend .fa-star-o !optional; } -.icon-user { @extend .fa-user !optional; } -.icon-film { @extend .fa-film !optional; } -.icon-th-large { @extend .fa-th-large !optional; } -.icon-th { @extend .fa-th !optional; } -.icon-th-list { @extend .fa-th-list !optional; } -.icon-ok { @extend .fa-check !optional; } -.icon-remove { @extend .fa-times !optional; } -.icon-zoom-in { @extend .fa-search-plus !optional; } -.icon-zoom-out { @extend .fa-search-minus !optional; } -.icon-off { @extend .fa-power-off !optional; } -.icon-signal { @extend .fa-signal !optional; } -.icon-cog { @extend .fa-cog !optional; } -.icon-trash { @extend .fa-trash-o !optional; } - -.icon-home { @extend .fa-home !optional; } -.icon-file { @extend .fa-file !optional; } -.icon-time { @extend .fa-clock-o !optional; } -.icon-road { @extend .fa-road !optional; } -.icon-download-alt { @extend .fa-download !optional; } -.icon-download { @extend .fa-download !optional; } -.icon-upload { @extend .fa-upload !optional; } -.icon-inbox { @extend .fa-inbox !optional; } -.icon-play-circle { @extend .fa-play-circle !optional; } -.icon-repeat { @extend .fa-repeat !optional; } -.icon-refresh { @extend .fa-refresh !optional; } -.icon-list-alt { @extend .fa-list-alt !optional; } -.icon-lock { @extend .fa-lock !optional; } -.icon-flag { @extend .fa-flag !optional; } -.icon-headphones { @extend .fa-headphones !optional; } -.icon-volume-off { @extend .fa-volume-off !optional; } -.icon-volume-down { @extend .fa-volume-down !optional; } -.icon-volume-up { @extend .fa-volume-up !optional; } -.icon-qrcode { @extend .fa-qrcode !optional; } -.icon-barcode { @extend .fa-barcode !optional; } - -.icon-tag { @extend .fa-tag !optional; } -.icon-tags { @extend .fa-tags !optional; } -.icon-book { @extend .fa-book !optional; } -.icon-bookmark { @extend .fa-bookmark !optional; } -.icon-print { @extend .fa-print !optional; } -.icon-camera { @extend .fa-camera !optional; } -.icon-font { @extend .fa-font !optional; } -.icon-bold { @extend .fa-bold !optional; } -.icon-italic { @extend .fa-italic !optional; } -.icon-text-height { @extend .fa-text-height !optional; } -.icon-text-width { @extend .fa-text-width !optional; } -.icon-align-left { @extend .fa-align-left !optional; } -.icon-align-center { @extend .fa-align-center !optional; } -.icon-align-right { @extend .fa-align-right !optional; } -.icon-align-justify { @extend .fa-align-justify !optional; } -.icon-list { @extend .fa-list !optional; } -.icon-indent-left { @extend .fa-indent !optional; } -.icon-indent-right { @extend .fa-dedent !optional; } -.icon-facetime-video { @extend .fa-video-camera !optional; } -.icon-picture { @extend .fa-picture-o !optional; } - -.icon-pencil { @extend .fa-pencil !optional; } -.icon-map-marker { @extend .fa-map-marker !optional; } -.icon-adjust { @extend .fa-adjust !optional; } -.icon-tint { @extend .fa-tint !optional; } -.icon-edit { @extend .fa-edit !optional; } -.icon-share { @extend .fa-share-square-o !optional; } -.icon-check { @extend .fa-check !optional; } -.icon-move { @extend .fa-arrows !optional; } -.icon-step-backward { @extend .fa-step-backward !optional; } -.icon-fast-backward { @extend .fa-fast-backward !optional; } -.icon-backward { @extend .fa-backward !optional; } -.icon-play { @extend .fa-play !optional; } -.icon-pause { @extend .fa-pause !optional; } -.icon-stop { @extend .fa-stop !optional; } -.icon-forward { @extend .fa-forward !optional; } -.icon-fast-forward { @extend .fa-fast-forward !optional; } -.icon-step-forward { @extend .fa-step-forward !optional; } -.icon-eject { @extend .fa-eject !optional; } -.icon-chevron-left { @extend .fa-chevron-left !optional; } -.icon-chevron-right { @extend .fa-chevron-right !optional; } - -.icon-plus-sign { @extend .fa-plus-circle !optional; } -.icon-minus-sign { @extend .fa-minus-circle !optional; } -.icon-remove-sign { @extend .fa-times-circle !optional; } -.icon-ok-sign { @extend .fa-check-circle !optional; } -.icon-question-sign { @extend .fa-question-circle !optional; } -.icon-info-sign { @extend .fa-info-circle !optional; } -.icon-screenshot { @extend .fa-crosshairs !optional; } -.icon-remove-circle { @extend .fa-times-circle-o !optional; } -.icon-ok-circle { @extend .fa-check-circle-o !optional; } -.icon-ban-circle { @extend .fa-ban !optional; } -.icon-arrow-left { @extend .fa-arrow-left !optional; } -.icon-arrow-right { @extend .fa-arrow-right !optional; } -.icon-arrow-up { @extend .fa-arrow-up !optional; } -.icon-arrow-down { @extend .fa-arrow-down !optional; } -.icon-share-alt { @extend .fa-share !optional; } -.icon-resize-full { @extend .fa-expand !optional; } -.icon-resize-small { @extend .fa-compress !optional; } -.icon-plus { @extend .fa-plus !optional; } -.icon-minus { @extend .fa-minus !optional; } -.icon-asterisk { @extend .fa-asterisk !optional; } - -.icon-exclamation-sign { @extend .fa-exclamation-circle !optional; } -.icon-gift { @extend .fa-gift !optional; } -.icon-leaf { @extend .fa-leaf !optional; } -.icon-fire { @extend .fa-fire !optional; } -.icon-eye-open { @extend .fa-eye !optional; } -.icon-eye-close { @extend .fa-eye-slash !optional; } -.icon-warning-sign { @extend .fa-warning !optional; } -.icon-plane { @extend .fa-plane !optional; } -.icon-calendar { @extend .fa-calendar !optional; } -.icon-random { @extend .fa-random !optional; } -.icon-comment { @extend .fa-comment !optional; } -.icon-magnet { @extend .fa-magnet !optional; } -.icon-chevron-up { @extend .fa-chevron-up !optional; } -.icon-chevron-down { @extend .fa-chevron-down !optional; } -.icon-retweet { @extend .fa-retweet !optional; } -.icon-shopping-cart { @extend .fa-shopping-cart !optional; } -.icon-folder-close { @extend .fa-folder !optional; } -.icon-folder-open { @extend .fa-folder-open !optional; } -.icon-resize-vertical { @extend .fa-arrows-v !optional; } -.icon-resize-horizontal { @extend .fa-arrows-h !optional; } - -.icon-hdd { @extend .fa-hdd-o !optional; } -.icon-bullhorn { @extend .fa-bullhorn !optional; } -.icon-bell { @extend .fa-bell !optional; } -.icon-certificate { @extend .fa-certificate !optional; } -.icon-thumbs-up { @extend .fa-thumbs-up !optional; } -.icon-thumbs-down { @extend .fa-thumbs-down !optional; } -.icon-hand-right { @extend .fa-hand-o-right !optional; } -.icon-hand-left { @extend .fa-hand-o-left !optional; } -.icon-hand-up { @extend .fa-hand-o-up !optional; } -.icon-hand-down { @extend .fa-hand-o-down !optional; } -.icon-circle-arrow-right { @extend .fa-arrow-circle-right !optional; } -.icon-circle-arrow-left { @extend .fa-arrow-circle-left !optional; } -.icon-circle-arrow-up { @extend .fa-arrow-circle-up !optional; } -.icon-circle-arrow-down { @extend .fa-arrow-circle-down !optional; } -.icon-globe { @extend .fa-globe !optional; } -.icon-wrench { @extend .fa-wrench !optional; } -.icon-tasks { @extend .fa-tasks !optional; } -.icon-filter { @extend .fa-filter !optional; } -.icon-briefcase { @extend .fa-briefcase !optional; } -.icon-fullscreen { @extend .fa-arrows-alt !optional; } - -.icon-white { color:white; } diff --git a/app/assets/stylesheets/rails_admin/base/theming.scss b/app/assets/stylesheets/rails_admin/base/theming.scss deleted file mode 100644 index 36ab5fe433..0000000000 --- a/app/assets/stylesheets/rails_admin/base/theming.scss +++ /dev/null @@ -1,341 +0,0 @@ -/* - RailsAdmin main CSS - Keep it clean, people -*/ - -$avatar-size: 30px; - -body.rails_admin { - - .thumbnail { - display: inline-block; - } - - /* room for upper navbar */ - padding-top: $navbar-height; - - .navbar { - .edit_user_root_link { - position: relative; - - img { - position: absolute; - top: ((40px - $avatar-size) / 2); - - & + span { - margin-left: ($avatar-size + 5px); - } - } - } - } - - .sidebar-nav { - padding: 0; - background-color: $navbar-default-bg; - - .dropdown-header { - padding: ($grid-gutter-width / 2) ($grid-gutter-width / 2) 3px; - font-weight: bold; - color: $gray; - text-transform: uppercase; - } - - > .nav-stacked > li > a { - padding: $padding-base-vertical ($grid-gutter-width / 2); - border-radius: 0; - - &.nav-level-1 { - padding-left: $grid-gutter-width; - } - - &.nav-level-2 { - padding-left: ($grid-gutter-width * 1.5); - } - - &.nav-level-3 { - padding-left: ($grid-gutter-width * 2); - } - } - } - - label.input-group-addon { - cursor: pointer; - } - - /* fat labels in forms */ - .label.form-label { - padding: 7px 7px 8px 7px; - } - - /* new/edit/export forms */ - .form-horizontal { - /* hide hidden fields controls by default */ - .control-group .hidden_type { - display:none; - } - - .control-group { - .img-thumbnail { - margin-bottom: 10px; - } - - .btn-remove-image { - &.btn-info { - margin-top: 10px; - } - } - } - - legend { - cursor:pointer; - i { - vertical-align: inherit !important; - } - } - - &.denser { - margin-top: 30px; - .controls .nav { - margin: 30px 0 0; - } - .remove_nested_fields, - .remove_nested_one_fields { - position:absolute; - } - p { - font-size: 12px; - line-height: 16px; - } - .help-block { - // margin-top: 0px; - } - .input-prepend, .input-append { - margin-bottom: 0px; - label { - margin-bottom: 0px; - } - } - legend { - margin-bottom: 30px; - } - } - - /* We want input size to be used, unfixate input width */ - input, textarea { - width:auto; - } - .help-block { - clear:left; - } - input[type=file] { - display:block; - } - - /* nested forms */ - .tab-content { - clear: both; - - .tab-pane { - @include clearfix; - border-left: 5px solid #049cdb; - position: relative; - - fieldset { - padding-top: 30px; - } - - .form-group { - margin-right: 0; - margin-left: -5px; /* Balance border-left */ - } - - & > .remove_nested_fields, - & > .remove_nested_one_fields { - display:block; - opacity: 0; - position: absolute; - top: 0px; - left: 5px; - -webkit-transition: opacity .15s ease; - transition: opacity .15s ease; - } - &:hover > .remove_nested_fields, - &:hover > .remove_nested_one_fields, - & > .remove_nested_one_fields.active { - opacity: 1; - } - } - } - - /* double nesting, guys. Easy. */ - .tab-content .tab-content .tab-pane { - border-color: lighten(#049cdb, 20%); - margin-left: 10px; - - .form-group { - margin-left: -15px; /* Balance border-left */ - } - } - - /* triple nesting!*/ - .tab-content .tab-content .tab-content .tab-pane { - border-color:lighten(#049cdb, 30%); - margin-left: 20px; - - .form-group { - margin-left: -25px; /* Balance border-left */ - } - } - - /* totally awesome! nesting on 4 levels! */ - .tab-content .tab-content .tab-content .tab-content .tab-pane { - border-color:lighten(#049cdb, 40%); - margin-left: 30px; - - .form-group { - margin-left: -35px; /* Balance border-left */ - } - } - } - - // .modal { - // width: 750px; - // margin-left: -375px; - // } - - /* nested nav */ - .nav { - .nav-level-1 { - padding-left:30px; - } - .nav-level-2 { - padding-left:60px; - } - .nav-level-3 { - padding-left:90px; - } - } - - /* tab links should match icon color */ - .nav.nav-tabs { - margin-bottom: 20px; - - li.icon a { - color: #000; - } - } - - /* Table cells behaviour */ - .table { - // Cells - > thead, - > tbody, - > tfoot { - > tr { - > th { - border-bottom: 0px; - } - } - } - .thumbnail { - float:left; - & + * { - clear:left; - } - } - /* backport of Bootstrap 1.4 table headers */ - .header { - cursor: pointer; - &:after { - content: ""; - float: right; - margin-top: 7px; - border-width: 0 4px 4px; - border-style: solid; - border-color: #000 transparent; - visibility: hidden; - } - } - .headerSortUp, - .headerSortDown { - background-color: rgba(141,192,219,.25); - text-shadow: 0 1px 1px rgba(255,255,255,.75); - } - .header:hover { - &:after { - visibility:visible; - } - } - .headerSortDown, - .headerSortDown:hover { - &:after { - visibility:visible; - @include opacity(60); - } - } - .headerSortUp { - &:after { - border-bottom: none; - border-left: 4px solid transparent; - border-right: 4px solid transparent; - border-top: 4px solid #000; - visibility:visible; - @include box-shadow(none); - @include opacity(60); - } - } - - /* text-overflow when cell content > 150px */ - td { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width:150px; - } - - /* icons */ - td.links { - max-width: none; - .list-inline { - margin:0px; - li { display:inline-block; } - } - } - - /* Shrink to content width */ - .shrink { - width:1px; - white-space:nowrap; - } - - /* History */ - th.created_at { - width:160px; - } - th.username { - width:140px; - } - th.item { - width:60px; - } - } -} - -@media screen and (min-width: $grid-float-breakpoint) { - body.rails_admin { - .navbar { - .edit_user_root_link { - img { - top: (($navbar-height - $avatar-size) / 2); - } - } - } - - .sidebar-nav { - position: fixed; - top: $navbar-height; - bottom: 0; - overflow-y: auto; - } - } -} diff --git a/app/assets/stylesheets/rails_admin/base/variables.scss b/app/assets/stylesheets/rails_admin/base/variables.scss deleted file mode 100644 index c8bf463fde..0000000000 --- a/app/assets/stylesheets/rails_admin/base/variables.scss +++ /dev/null @@ -1 +0,0 @@ -/* RailsAdmin SASS variables */ diff --git a/app/assets/stylesheets/rails_admin/custom/mixins.scss b/app/assets/stylesheets/rails_admin/custom/mixins.scss index d760cb1c74..b124f2c9b8 100644 --- a/app/assets/stylesheets/rails_admin/custom/mixins.scss +++ b/app/assets/stylesheets/rails_admin/custom/mixins.scss @@ -6,6 +6,6 @@ Available mixins to use/override: https://github.com/twbs/bootstrap-sass/tree/master/assets/stylesheets/bootstrap/mixins - https://github.com/sferik/rails_admin/blob/master/app/assets/stylesheets/rails_admin/base/mixins.scss + https://github.com/railsadminteam/rails_admin/blob/master/src/rails_admin/styles/base/mixins.scss Plus the ones from your theme. */ diff --git a/app/assets/stylesheets/rails_admin/custom/variables.scss b/app/assets/stylesheets/rails_admin/custom/variables.scss index 782d362b38..c1016477ca 100644 --- a/app/assets/stylesheets/rails_admin/custom/variables.scss +++ b/app/assets/stylesheets/rails_admin/custom/variables.scss @@ -6,7 +6,7 @@ Available variables to use/override: https://github.com/twbs/bootstrap-sass/blob/master/assets/stylesheets/bootstrap/_variables.scss - https://github.com/sferik/rails_admin/blob/master/app/assets/stylesheets/rails_admin/base/variables.scss + https://github.com/railsadminteam/rails_admin/blob/master/src/rails_admin/styles/base/variables.scss Plus the ones from your themes. Test me: pink links diff --git a/app/assets/stylesheets/rails_admin/ra.filtering-multiselect.scss b/app/assets/stylesheets/rails_admin/ra.filtering-multiselect.scss deleted file mode 100644 index e796076535..0000000000 --- a/app/assets/stylesheets/rails_admin/ra.filtering-multiselect.scss +++ /dev/null @@ -1,88 +0,0 @@ -.ra-multiselect { - - .ra-multiselect-header { - margin-bottom: 5px; - clear: right; - .help { - float: left; - font-size: 10px; - } - .ui-icon { - position: relative; - top: 10px; - left: 3px; - } - } - - .ra-multiselect-search { - width: 180px; - } - - .ra-multiselect-column { - float: left; - .wrapper { - overflow-x: auto; - select { - height: auto; - min-height: 120px; - margin-bottom: 5px; - min-width: 200px; - } - } - } - - .ra-multiselect-center { - width: 30px; - padding-top: 15px; - text-align: center; - .ui-icon { - margin: 5px auto; - } - } - - span.ui-icon { - margin-right: 4px; - } - .ui-icon { - cursor: pointer; - display: inline-block; - vertical-align: top; - &.ui-icon-circle-triangle-e { - background-position: -16px -32px !important; - background-image: image-url("rails_admin/multiselect/icon_sprite.png"); - } - &.ui-icon-circle-triangle-e:hover { - background-position: -16px -48px !important; - background-image: image-url('rails_admin/multiselect/icon_sprite.png'); - } - &.ui-icon-circle-triangle-w { - background-position: 0px -32px !important; - background-image: image-url('rails_admin/multiselect/icon_sprite.png'); - } - &.ui-icon-circle-triangle-w:hover { - background-position: 0px -48px !important; - background-image: image-url('rails_admin/multiselect/icon_sprite.png'); - } - &.ui-icon-circle-triangle-n { - background-position: 0px 0px !important; - margin-left: 1px; - background-image: image-url('rails_admin/multiselect/ui-icon-circle-triangle-n-light.png'); - } - &.ui-icon-circle-triangle-n:hover { - background-position: 0px 0px !important; - margin-left: 1px; - background-image: image-url('rails_admin/multiselect/ui-icon-circle-triangle-n-dark.png'); - } - &.ui-icon-circle-triangle-s { - background-position: 0px 0px !important; - margin-left: 1px; - background-image: image-url('rails_admin/multiselect/ui-icon-circle-triangle-s-light.png'); - } - &.ui-icon-circle-triangle-s:hover { - background-position: 0px 0px !important; - margin-left: 1px; - background-image: image-url('rails_admin/multiselect/ui-icon-circle-triangle-s-dark.png'); - } - } -} - diff --git a/app/assets/stylesheets/rails_admin/ra.sidescroll.scss b/app/assets/stylesheets/rails_admin/ra.sidescroll.scss deleted file mode 100644 index 3c30b5e533..0000000000 --- a/app/assets/stylesheets/rails_admin/ra.sidescroll.scss +++ /dev/null @@ -1,29 +0,0 @@ -.ra-sidescroll { - margin-bottom: 20px; - overflow-x: auto; - .table { - margin-bottom: 0; - } - - .ra-sidescroll-frozen { - position: sticky; - - // border-right isn't sticky - &.last-of-frozen { - box-shadow: -1px 0 0 0 $table-border-color inset; - padding-right: $table-condensed-cell-padding + 1px; - } - } - - // Remove transparency on frozen cells. - $table-bg-default: if($table-bg == transparent, if($body-bg == transparent, #fff, $body-bg), $table-bg) !default; - .table-striped > tbody > tr:nth-child(even) > td, .table-striped > thead > tr > th { - background-color: $table-bg-default; - } - $table-bg-header-sort: #e2eff6 !default; - .table .ra-sidescroll-frozen { - &.headerSortUp, &.headerSortDown { - background-color: $table-bg-header-sort; - } - } -} diff --git a/app/assets/stylesheets/rails_admin/ra.widgets.scss b/app/assets/stylesheets/rails_admin/ra.widgets.scss deleted file mode 100644 index 9b690fc25e..0000000000 --- a/app/assets/stylesheets/rails_admin/ra.widgets.scss +++ /dev/null @@ -1,17 +0,0 @@ -iframe.wysihtml5-sandbox{ - height: 250px !important; - width: 99.7% !important; -} - -iframe.wysihtml5-sandbox, .wysihtml5-editor{ - border: 1px solid #ccc !important; -} - -.controls img.preview{ - max-height: 200px; - max-width: 300px; -} - -.controls.ui-sortable .sortables{ - cursor: move; -} \ No newline at end of file diff --git a/app/assets/stylesheets/rails_admin/rails_admin.scss.erb b/app/assets/stylesheets/rails_admin/rails_admin.scss.erb deleted file mode 100644 index 9b12ea1f8d..0000000000 --- a/app/assets/stylesheets/rails_admin/rails_admin.scss.erb +++ /dev/null @@ -1,110 +0,0 @@ -// Issue 1956 (https://github.com/sferik/rails_admin/issues/1956) -//= depend_on_asset "rails_admin/bootstrap/glyphicons-halflings.png" -//= depend_on_asset "rails_admin/bootstrap/glyphicons-halflings-white.png" -//= depend_on_asset "rails_admin/aristo/images/bg_fallback.png" -//= depend_on_asset "rails_admin/aristo/images/ui-icons_222222_256x240.png" -//= depend_on_asset "rails_admin/aristo/images/ui-icons_454545_256x240.png" -//= depend_on_asset "rails_admin/aristo/images/icon_sprite.png" -//= depend_on_asset "rails_admin/aristo/images/slider_handles.png" -//= depend_on_asset "rails_admin/aristo/images/progress_bar.gif" -//= depend_on_asset "rails_admin/multiselect/icon_sprite.png" -//= depend_on_asset "rails_admin/multiselect/ui-icon-circle-triangle-n-light.png" -//= depend_on_asset "rails_admin/multiselect/ui-icon-circle-triangle-n-dark.png" -//= depend_on_asset "rails_admin/multiselect/ui-icon-circle-triangle-s-light.png" -//= depend_on_asset "rails_admin/multiselect/ui-icon-circle-triangle-s-dark.png" -//= depend_on_asset "rails_admin/colorpicker/select2.png" -//= depend_on_asset "rails_admin/colorpicker/custom_background.png" -//= depend_on_asset "rails_admin/colorpicker/colorpicker_overlay.png" -//= depend_on_asset "rails_admin/colorpicker/colorpicker_select.gif" -//= depend_on_asset "rails_admin/colorpicker/custom_indic.gif" -//= depend_on_asset "rails_admin/colorpicker/custom_hex.png" -//= depend_on_asset "rails_admin/colorpicker/custom_rgb_r.png" -//= depend_on_asset "rails_admin/colorpicker/custom_rgb_g.png" -//= depend_on_asset "rails_admin/colorpicker/custom_rgb_b.png" -//= depend_on_asset "rails_admin/colorpicker/custom_hsb_h.png" -//= depend_on_asset "rails_admin/colorpicker/custom_hsb_s.png" -//= depend_on_asset "rails_admin/colorpicker/custom_hsb_b.png" -//= depend_on_asset "rails_admin/colorpicker/custom_submit.png" - - -@charset "UTF-8"; - -<% - theme = ENV['RAILS_ADMIN_THEME'] || :default -%> - -/*** Variables ***/ - -@import "rails_admin/custom/variables"; -@import "rails_admin/bootstrap/variables"; -@import "rails_admin/base/variables"; -@import "rails_admin/themes/<%= theme %>/variables"; - -/*** Mixins ***/ - -@import "rails_admin/bootstrap/mixins"; -@import "rails_admin/base/mixins"; -@import "rails_admin/themes/<%= theme %>/mixins"; -@import "rails_admin/custom/mixins"; - -/*** Reset ***/ -<%# @import "rails_admin/bootstrap/reset"; %> -/*** Libraries ***/ - -@import "rails_admin/aristo/jquery-ui-1.8.7.custom"; -@import "rails_admin/bootstrap-datetimepicker-build"; -@import "rails_admin/ra.filtering-multiselect"; -@import "rails_admin/ra.widgets"; -@import "rails_admin/ra.sidescroll"; -@import "rails_admin/jquery.colorpicker"; - - -/*** Font-awesome ***/ - -@import 'rails_admin/font-awesome'; -@import 'rails_admin/base/font-awesome-4-compability'; - -/*** Bootstrap Theming ***/ - -@import "rails_admin/bootstrap/normalize"; -@import "rails_admin/bootstrap/scaffolding"; -@import "rails_admin/bootstrap/grid"; -<%# @import "rails_admin/bootstrap/layouts"; %> -@import "rails_admin/bootstrap/type"; -@import "rails_admin/bootstrap/forms"; -@import "rails_admin/bootstrap/tables"; -@import "rails_admin/bootstrap/dropdowns"; -@import "rails_admin/bootstrap/wells"; -@import "rails_admin/bootstrap/component-animations"; -@import "rails_admin/bootstrap/close"; -@import "rails_admin/bootstrap/buttons"; -@import "rails_admin/bootstrap/button-groups"; -@import "rails_admin/bootstrap/input-groups"; -@import "rails_admin/bootstrap/alerts"; -@import "rails_admin/bootstrap/navs"; -@import "rails_admin/bootstrap/navbar"; -@import "rails_admin/bootstrap/breadcrumbs"; -@import "rails_admin/bootstrap/pagination"; -@import "rails_admin/bootstrap/pager"; -@import "rails_admin/bootstrap/modals"; -@import "rails_admin/bootstrap/tooltip"; -@import "rails_admin/bootstrap/popovers"; -@import "rails_admin/bootstrap/thumbnails"; -@import "rails_admin/bootstrap/labels"; -@import "rails_admin/bootstrap/panels"; -@import "rails_admin/bootstrap/badges"; -@import "rails_admin/bootstrap/progress-bars"; -<%# @import "rails_admin/bootstrap/accordion"; %> -@import "rails_admin/bootstrap/carousel"; -<%# @import "rails_admin/bootstrap/hero-unit"; %> -@import "rails_admin/bootstrap/utilities"; -@import "rails_admin/bootstrap/responsive-utilities"; - -/*** RailsAdmin Theming ***/ - -@import "rails_admin/base/theming"; -@import "rails_admin/themes/<%= theme %>/theming"; -@import "rails_admin/custom/theming"; - - - diff --git a/app/assets/stylesheets/rails_admin/themes/cerulean/theming.scss b/app/assets/stylesheets/rails_admin/themes/cerulean/theming.scss deleted file mode 100644 index b7bd500d78..0000000000 --- a/app/assets/stylesheets/rails_admin/themes/cerulean/theming.scss +++ /dev/null @@ -1,133 +0,0 @@ -// Cerulean 3.3.1 -// Bootswatch -// .rails_admin .sidebar-nav added to prevent same link color -// ------------------------------------------------------------------ - -@mixin btn-shadow($color){ - @include gradient-vertical-three-colors(lighten($color, 8%), $color, 60%, darken($color, 4%)); - filter: none; - border-bottom: 1px solid darken($color, 10%); -} - -// Navbar ===================================================================== - -.navbar { - @include btn-shadow($navbar-default-bg); - filter: none; - @include box-shadow(0 1px 10px rgba(0, 0, 0, 0.1)); -} - -.navbar.navbar-default .badge { - background-color: #fff; - color: $navbar-default-bg; - } - -.navbar.navbar-inverse , .rails_admin .sidebar-nav { - @include gradient-vertical-three-colors(lighten($navbar-inverse-bg, 8%), lighten($navbar-inverse-bg, 4%), 60%, darken($navbar-inverse-bg, 2%)); - filter: none; - border-bottom: 1px solid darken($navbar-inverse-bg, 10%); - } - - -.navbar.navbar-inverse .badge { - background-color: #fff; - color: $navbar-inverse-bg; -} - -.navbar .navbar-nav > li > a, -.navbar .navbar-brand { - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); - } - - -@media (max-width: $grid-float-breakpoint-max) { - - .navbar { - - .dropdown-header { - color: #fff; - } - } -} - -// Buttons ==================================================================== - -.btn { - - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); - - .caret { - border-top-color: #fff; - } -} - -.btn-default { - - @include btn-shadow($btn-default-bg); - - &:hover { - color: $btn-default-color; - } - - .caret { - border-top-color: $text-color; - } -} - -.btn-default { - @include btn-shadow($btn-default-bg); -} - -.btn-primary { - @include btn-shadow($btn-primary-bg); -} - -.btn-success { - @include btn-shadow($btn-success-bg); -} - -.btn-info { - @include btn-shadow($btn-info-bg); -} - -.btn-warning { - @include btn-shadow($btn-warning-bg); -} - -.btn-danger { - @include btn-shadow($btn-danger-bg); -} - -// Typography ================================================================= - -// Tables ===================================================================== -table, -.table { - > thead > tr > th { - - color: $headings-color; - } - -} - -// Forms ====================================================================== - -// Navs ======================================================================= - -// Indicators ================================================================= - -// Progress bars ============================================================== - -// Containers ================================================================= - -.panel-primary, -.panel-success, -.panel-warning, -.panel-danger, -.panel-info { - - .panel-heading, - .panel-title { - color: #fff; - } -} diff --git a/app/assets/stylesheets/rails_admin/themes/cerulean/variables.scss b/app/assets/stylesheets/rails_admin/themes/cerulean/variables.scss deleted file mode 100644 index 5f4807394f..0000000000 --- a/app/assets/stylesheets/rails_admin/themes/cerulean/variables.scss +++ /dev/null @@ -1,857 +0,0 @@ -// Cerulean 3.3.1 -// Variables -// -// -------------------------------------------------- - - -//== Colors -// -//## Gray and brand colors for use across Bootstrap. - -$gray-base: #000; -$gray-darker: lighten($gray-base, 13.5%); // #222 -$gray-dark: lighten($gray-base, 20%); // #333 -$gray: lighten($gray-base, 33.5%); // #555 -$gray-light: lighten($gray-base, 60%); // #999 -$gray-lighter: lighten($gray-base, 93.5%); // #eee - -$brand-info: #2FA4E7; // default $brand-primary -$brand-success: #73A839; -$brand-primary: #033C73; // default $brand-info -$brand-warning: #DD5600; -$brand-danger: #C71C22; - - -//== Scaffolding -// -//## Settings for some of the most global styles. - -//** Background color for ``. -$body-bg: #fff; -//** Global text color on ``. -$text-color: $gray; - -//** Global textual link color. -$link-color: $brand-primary; -//** Link hover color set via `darken()` function. -$link-hover-color: darken($link-color, 15%); -//** Link hover decoration. -$link-hover-decoration: underline; - - -//== Typography -// -//## Font, line-height, and color for body text, headings, and more. - -$font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif; -$font-family-serif: Georgia, "Times New Roman", Times, serif; -//** Default monospace fonts for ``, ``, and `
    `.
    -$font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
    -$font-family-base:        $font-family-sans-serif;
    -
    -$font-size-base:          14px;
    -$font-size-large:         ceil(($font-size-base * 1.25)); // ~18px
    -$font-size-small:         ceil(($font-size-base * 0.85)); // ~12px
    -
    -$font-size-h1:            floor(($font-size-base * 2.6)); // ~36px
    -$font-size-h2:            floor(($font-size-base * 2.15)); // ~30px
    -$font-size-h3:            ceil(($font-size-base * 1.7)); // ~24px
    -$font-size-h4:            ceil(($font-size-base * 1.25)); // ~18px
    -$font-size-h5:            $font-size-base;
    -$font-size-h6:            ceil(($font-size-base * 0.85)); // ~12px
    -
    -//** Unit-less `line-height` for use in components like buttons.
    -$line-height-base:        1.428571429; // 20/14
    -//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
    -$line-height-computed:    floor(($font-size-base * $line-height-base)); // ~20px
    -
    -//** By default, this inherits from the ``.
    -$headings-font-family:    $font-family-base;
    -$headings-font-weight:    500;
    -$headings-line-height:    1.1;
    -$headings-color:          #317EAC;
    -
    -
    -//== Iconography
    -//
    -//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
    -
    -//** Load fonts from this directory.
    -$icon-font-path:          "../fonts/";
    -//** File name for all font files.
    -$icon-font-name:          "glyphicons-halflings-regular";
    -//** Element ID within SVG icon file.
    -$icon-font-svg-id:        "glyphicons_halflingsregular";
    -
    -
    -//== Components
    -//
    -//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
    -
    -$padding-base-vertical:     8px;
    -$padding-base-horizontal:   12px;
    -
    -$padding-large-vertical:    14px;
    -$padding-large-horizontal:  16px;
    -
    -$padding-small-vertical:    5px;
    -$padding-small-horizontal:  10px;
    -
    -$padding-xs-vertical:       1px;
    -$padding-xs-horizontal:     5px;
    -
    -$line-height-large:         1.33;
    -$line-height-small:         1.5;
    -
    -$border-radius-base:        0px;
    -$border-radius-large:       0px;
    -$border-radius-small:       0px;
    -
    -//** Global color for active items (e.g., navs or dropdowns).
    -$component-active-color:    #fff;
    -//** Global background color for active items (e.g., navs or dropdowns).
    -$component-active-bg:       $brand-primary;
    -
    -//** Width of the `border` for generating carets that indicator dropdowns.
    -$caret-width-base:          4px;
    -//** Carets increase slightly in size for larger components.
    -$caret-width-large:         5px;
    -
    -
    -//== Tables
    -//
    -//## Customizes the `.table` component with basic values, each used across all table variations.
    -
    -//** Padding for ``s and ``s.
    -$table-cell-padding:            8px;
    -//** Padding for cells in `.table-condensed`.
    -$table-condensed-cell-padding:  5px;
    -
    -//** Default background color used for all tables.
    -$table-bg:                      transparent;
    -//** Background color used for `.table-striped`.
    -$table-bg-accent:               #f9f9f9;
    -//** Background color used for `.table-hover`.
    -$table-bg-hover:                #f5f5f5;
    -$table-bg-active:               $table-bg-hover;
    -
    -//** Border color for table and cell borders.
    -$table-border-color:            #ddd;
    -
    -
    -//== Buttons
    -//
    -//## For each of Bootstrap's buttons, define text, background and border color.
    -
    -$btn-font-weight:                normal;
    -
    -$btn-default-color:              $text-color;
    -$btn-default-bg:                 #fff;
    -$btn-default-border:             rgba(0, 0, 0, 0.1);
    -
    -$btn-primary-color:              #fff;
    -$btn-primary-bg:                 $brand-primary;
    -$btn-primary-border:             $btn-primary-bg;
    -
    -$btn-success-color:              #fff;
    -$btn-success-bg:                 $brand-success;
    -$btn-success-border:             $btn-success-bg;
    -
    -$btn-info-color:                 #fff;
    -$btn-info-bg:                    $brand-info;
    -$btn-info-border:                $btn-info-bg;
    -
    -$btn-warning-color:              #fff;
    -$btn-warning-bg:                 $brand-warning;
    -$btn-warning-border:             $btn-warning-bg;
    -
    -$btn-danger-color:               #fff;
    -$btn-danger-bg:                  $brand-danger;
    -$btn-danger-border:              $btn-danger-bg;
    -
    -$btn-link-disabled-color:        $gray-light;
    -
    -
    -//== Forms
    -//
    -//##
    -
    -//** `` background color
    -$input-bg:                       #fff;
    -//** `` background color
    -$input-bg-disabled:              $gray-lighter;
    -
    -//** Text color for ``s
    -$input-color:                    $text-color;
    -//** `` border color
    -$input-border:                   #ccc;
    -
    -// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4
    -//** Default `.form-control` border radius
    -$input-border-radius:            $border-radius-base;
    -//** Large `.form-control` border radius
    -$input-border-radius-large:      $border-radius-large;
    -//** Small `.form-control` border radius
    -$input-border-radius-small:      $border-radius-small;
    -
    -//** Border color for inputs on focus
    -$input-border-focus:             #66afe9;
    -
    -//** Placeholder text color
    -$input-color-placeholder:        $gray-light;
    -
    -//** Default `.form-control` height
    -$input-height-base:              ($line-height-computed + ($padding-base-vertical * 2) + 2);
    -//** Large `.form-control` height
    -$input-height-large:             (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2);
    -//** Small `.form-control` height
    -$input-height-small:             (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2);
    -
    -$legend-color:                   $text-color;
    -$legend-border-color:            #e5e5e5;
    -
    -//** Background color for textual input addons
    -$input-group-addon-bg:           $gray-lighter;
    -//** Border color for textual input addons
    -$input-group-addon-border-color: $input-border;
    -
    -//** Disabled cursor for form controls and buttons.
    -$cursor-disabled:                not-allowed;
    -
    -
    -//== Dropdowns
    -//
    -//## Dropdown menu container and contents.
    -
    -//** Background for the dropdown menu.
    -$dropdown-bg:                    #fff;
    -//** Dropdown menu `border-color`.
    -$dropdown-border:                rgba(0,0,0,.15);
    -//** Dropdown menu `border-color` **for IE8**.
    -$dropdown-fallback-border:       #ccc;
    -//** Divider color for between dropdown items.
    -$dropdown-divider-bg:            #e5e5e5;
    -
    -//** Dropdown link text color.
    -$dropdown-link-color:            $gray-dark;
    -//** Hover color for dropdown links.
    -$dropdown-link-hover-color:      #fff;
    -//** Hover background for dropdown links.
    -$dropdown-link-hover-bg:         $component-active-bg;
    -
    -//** Active dropdown menu item text color.
    -$dropdown-link-active-color:     #fff;
    -//** Active dropdown menu item background color.
    -$dropdown-link-active-bg:        $component-active-bg;
    -
    -//** Disabled dropdown menu item background color.
    -$dropdown-link-disabled-color:   $gray-light;
    -
    -//** Text color for headers within dropdown menus.
    -$dropdown-header-color:          $gray-light;
    -
    -//** Deprecated `$dropdown-caret-color` as of v3.1.0
    -$dropdown-caret-color:           #000;
    -
    -
    -//-- Z-index master list
    -//
    -// Warning: Avoid customizing these values. They're used for a bird's eye view
    -// of components dependent on the z-axis and are designed to all work together.
    -//
    -// Note: These variables are not generated into the Customizer.
    -
    -$zindex-navbar:            1000;
    -$zindex-dropdown:          1000;
    -$zindex-popover:           1060;
    -$zindex-tooltip:           1070;
    -$zindex-navbar-fixed:      1030;
    -$zindex-modal:             1040;
    -
    -
    -//== Media queries breakpoints
    -//
    -//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
    -
    -// Extra small screen / phone
    -//** Deprecated `$screen-xs` as of v3.0.1
    -$screen-xs:                  480px;
    -//** Deprecated `$screen-xs-min` as of v3.2.0
    -$screen-xs-min:              $screen-xs;
    -//** Deprecated `$screen-phone` as of v3.0.1
    -$screen-phone:               $screen-xs-min;
    -
    -// Small screen / tablet
    -//** Deprecated `$screen-sm` as of v3.0.1
    -$screen-sm:                  768px;
    -$screen-sm-min:              $screen-sm;
    -//** Deprecated `$screen-tablet` as of v3.0.1
    -$screen-tablet:              $screen-sm-min;
    -
    -// Medium screen / desktop
    -//** Deprecated `$screen-md` as of v3.0.1
    -$screen-md:                  992px;
    -$screen-md-min:              $screen-md;
    -//** Deprecated `$screen-desktop` as of v3.0.1
    -$screen-desktop:             $screen-md-min;
    -
    -// Large screen / wide desktop
    -//** Deprecated `$screen-lg` as of v3.0.1
    -$screen-lg:                  1200px;
    -$screen-lg-min:              $screen-lg;
    -//** Deprecated `$screen-lg-desktop` as of v3.0.1
    -$screen-lg-desktop:          $screen-lg-min;
    -
    -// So media queries don't overlap when required, provide a maximum
    -$screen-xs-max:              ($screen-sm-min - 1);
    -$screen-sm-max:              ($screen-md-min - 1);
    -$screen-md-max:              ($screen-lg-min - 1);
    -
    -
    -//== Grid system
    -//
    -//## Define your custom responsive grid.
    -
    -//** Number of columns in the grid.
    -$grid-columns:              12;
    -//** Padding between columns. Gets divided in half for the left and right.
    -$grid-gutter-width:         30px;
    -// Navbar collapse
    -//** Point at which the navbar becomes uncollapsed.
    -$grid-float-breakpoint:     $screen-sm-min;
    -//** Point at which the navbar begins collapsing.
    -$grid-float-breakpoint-max: ($grid-float-breakpoint - 1);
    -
    -
    -//== Container sizes
    -//
    -//## Define the maximum width of `.container` for different screen sizes.
    -
    -// Small screen / tablet
    -$container-tablet:             (720px + $grid-gutter-width);
    -//** For `$screen-sm-min` and up.
    -$container-sm:                 $container-tablet;
    -
    -// Medium screen / desktop
    -$container-desktop:            (940px + $grid-gutter-width);
    -//** For `$screen-md-min` and up.
    -$container-md:                 $container-desktop;
    -
    -// Large screen / wide desktop
    -$container-large-desktop:      (1140px + $grid-gutter-width);
    -//** For `$screen-lg-min` and up.
    -$container-lg:                 $container-large-desktop;
    -
    -
    -//== Navbar
    -//
    -//##
    -
    -// Basics of a navbar
    -$navbar-height:                    50px;
    -$navbar-margin-bottom:             $line-height-computed;
    -$navbar-border-radius:             $border-radius-base;
    -$navbar-padding-horizontal:        floor(($grid-gutter-width / 2));
    -$navbar-padding-vertical:          (($navbar-height - $line-height-computed) / 2);
    -$navbar-collapse-max-height:       340px;
    -
    -$navbar-default-color:             #ddd;
    -$navbar-default-bg:                $brand-primary;
    -$navbar-default-border:            darken($navbar-default-bg, 6.5%);
    -
    -// Navbar links
    -$navbar-default-link-color:                #fff;
    -$navbar-default-link-hover-color:          #fff;
    -$navbar-default-link-hover-bg:             darken($navbar-default-bg, 10%);
    -$navbar-default-link-active-color:         #fff;
    -$navbar-default-link-active-bg:            darken($navbar-default-bg, 10%);
    -$navbar-default-link-disabled-color:       #ddd;
    -$navbar-default-link-disabled-bg:          transparent;
    -
    -// Navbar brand label
    -$navbar-default-brand-color:               $navbar-default-link-color;
    -$navbar-default-brand-hover-color:         #fff;
    -$navbar-default-brand-hover-bg:            none;
    -
    -// Navbar toggle
    -$navbar-default-toggle-hover-bg:           darken($navbar-default-bg, 10%);
    -$navbar-default-toggle-icon-bar-bg:        #fff;
    -$navbar-default-toggle-border-color:       darken($navbar-default-bg, 10%);
    -
    -
    -// Inverted navbar
    -// Reset inverted navbar basics
    -$navbar-inverse-color:                      #fff;
    -$navbar-inverse-bg:                         $brand-info;
    -$navbar-inverse-border:                     darken($navbar-inverse-bg, 5%);
    -
    -// Inverted navbar links
    -$navbar-inverse-link-color:                 #fff;
    -$navbar-inverse-link-hover-color:           #fff;
    -$navbar-inverse-link-hover-bg:              darken($navbar-inverse-bg, 5%);
    -$navbar-inverse-link-active-color:          #fff;
    -$navbar-inverse-link-active-bg:             darken($navbar-inverse-bg, 5%);
    -$navbar-inverse-link-disabled-color:        #ccc;
    -$navbar-inverse-link-disabled-bg:           transparent;
    -
    -// Inverted navbar brand label
    -$navbar-inverse-brand-color:                $navbar-inverse-link-color;
    -$navbar-inverse-brand-hover-color:          #fff;
    -$navbar-inverse-brand-hover-bg:             none;
    -
    -// Inverted navbar toggle
    -$navbar-inverse-toggle-hover-bg:            darken($navbar-inverse-bg, 5%);
    -$navbar-inverse-toggle-icon-bar-bg:         #fff;
    -$navbar-inverse-toggle-border-color:        darken($navbar-inverse-bg, 5%);
    -
    -
    -//== Navs
    -//
    -//##
    -
    -//=== Shared nav styles
    -$nav-link-padding:                          10px 15px;
    -$nav-link-hover-bg:                         $gray-lighter;
    -
    -$nav-disabled-link-color:                   $gray-light;
    -$nav-disabled-link-hover-color:             $gray-light;
    -
    -//== Tabs
    -$nav-tabs-border-color:                     #ddd;
    -
    -$nav-tabs-link-hover-border-color:          $gray-lighter;
    -
    -$nav-tabs-active-link-hover-bg:             $body-bg;
    -$nav-tabs-active-link-hover-color:          $gray;
    -$nav-tabs-active-link-hover-border-color:   #ddd;
    -
    -$nav-tabs-justified-link-border-color:            #ddd;
    -$nav-tabs-justified-active-link-border-color:     $body-bg;
    -
    -//== Pills
    -$nav-pills-border-radius:                   $border-radius-base;
    -$nav-pills-active-link-hover-bg:            $component-active-bg;
    -$nav-pills-active-link-hover-color:         $component-active-color;
    -
    -
    -//== Pagination
    -//
    -//##
    -
    -$pagination-color:                     $link-color;
    -$pagination-bg:                        #fff;
    -$pagination-border:                    #ddd;
    -
    -$pagination-hover-color:               $link-hover-color;
    -$pagination-hover-bg:                  $gray-lighter;
    -$pagination-hover-border:              #ddd;
    -
    -$pagination-active-color:              $gray-light;
    -$pagination-active-bg:                 #f5f5f5;
    -$pagination-active-border:             $pagination-hover-border;
    -
    -$pagination-disabled-color:            $gray-light;
    -$pagination-disabled-bg:               #fff;
    -$pagination-disabled-border:           #ddd;
    -
    -
    -//== Pager
    -//
    -//##
    -
    -$pager-bg:                             $pagination-bg;
    -$pager-border:                         $pagination-border;
    -$pager-border-radius:                  15px;
    -
    -$pager-hover-bg:                       $pagination-hover-bg;
    -
    -$pager-active-bg:                      $pagination-active-bg;
    -$pager-active-color:                   $pagination-active-color;
    -
    -$pager-disabled-color:                 $gray-light;
    -
    -
    -//== Jumbotron
    -//
    -//##
    -
    -$jumbotron-padding:              30px;
    -$jumbotron-color:                inherit;
    -$jumbotron-bg:                   $gray-lighter;
    -$jumbotron-heading-color:        inherit;
    -$jumbotron-font-size:            ceil(($font-size-base * 1.5));
    -
    -
    -//== Form states and alerts
    -//
    -//## Define colors for form feedback states and, by default, alerts.
    -
    -$state-success-text:             #468847;
    -$state-success-bg:               #dff0d8;
    -$state-success-border:           darken(adjust-hue($state-success-bg, -10), 5%);
    -
    -$state-info-text:                #3a87ad;
    -$state-info-bg:                  #d9edf7;
    -$state-info-border:              darken(adjust-hue($state-info-bg, -10), 7%);
    -
    -$state-warning-text:             #c09853;
    -$state-warning-bg:               #fcf8e3;
    -$state-warning-border:           darken(adjust-hue($state-warning-bg, -10), 3%);
    -
    -$state-danger-text:              #b94a48;
    -$state-danger-bg:                #f2dede;
    -$state-danger-border:            darken(adjust-hue($state-danger-bg, -10), 3%);
    -
    -
    -//== Tooltips
    -//
    -//##
    -
    -//** Tooltip max width
    -$tooltip-max-width:           200px;
    -//** Tooltip text color
    -$tooltip-color:               #fff;
    -//** Tooltip background color
    -$tooltip-bg:                  rgba(0,0,0,.9);
    -$tooltip-opacity:             .9;
    -
    -//** Tooltip arrow width
    -$tooltip-arrow-width:         5px;
    -//** Tooltip arrow color
    -$tooltip-arrow-color:         $tooltip-bg;
    -
    -
    -//== Popovers
    -//
    -//##
    -
    -//** Popover body background color
    -$popover-bg:                          #fff;
    -//** Popover maximum width
    -$popover-max-width:                   276px;
    -//** Popover border color
    -$popover-border-color:                rgba(0,0,0,.2);
    -//** Popover fallback border color
    -$popover-fallback-border-color:       #ccc;
    -
    -//** Popover title background color
    -$popover-title-bg:                    darken($popover-bg, 3%);
    -
    -//** Popover arrow width
    -$popover-arrow-width:                 10px;
    -//** Popover arrow color
    -$popover-arrow-color:                 $popover-bg;
    -
    -//** Popover outer arrow width
    -$popover-arrow-outer-width:           ($popover-arrow-width + 1);
    -//** Popover outer arrow color
    -$popover-arrow-outer-color:           fadein($popover-border-color, 5%);
    -//** Popover outer arrow fallback color
    -$popover-arrow-outer-fallback-color:  darken($popover-fallback-border-color, 20%);
    -
    -
    -//== Labels
    -//
    -//##
    -
    -//** Default label background color
    -$label-default-bg:            $gray-light;
    -//** Primary label background color
    -$label-primary-bg:            $brand-primary;
    -//** Success label background color
    -$label-success-bg:            $brand-success;
    -//** Info label background color
    -$label-info-bg:               $brand-info;
    -//** Warning label background color
    -$label-warning-bg:            $brand-warning;
    -//** Danger label background color
    -$label-danger-bg:             $brand-danger;
    -
    -//** Default label text color
    -$label-color:                 #fff;
    -//** Default text color of a linked label
    -$label-link-hover-color:      #fff;
    -
    -
    -//== Modals
    -//
    -//##
    -
    -//** Padding applied to the modal body
    -$modal-inner-padding:         20px;
    -
    -//** Padding applied to the modal title
    -$modal-title-padding:         15px;
    -//** Modal title line-height
    -$modal-title-line-height:     $line-height-base;
    -
    -//** Background color of modal content area
    -$modal-content-bg:                             #fff;
    -//** Modal content border color
    -$modal-content-border-color:                   rgba(0,0,0,.2);
    -//** Modal content border color **for IE8**
    -$modal-content-fallback-border-color:          #999;
    -
    -//** Modal backdrop background color
    -$modal-backdrop-bg:           #000;
    -//** Modal backdrop opacity
    -$modal-backdrop-opacity:      .5;
    -//** Modal header border color
    -$modal-header-border-color:   #e5e5e5;
    -//** Modal footer border color
    -$modal-footer-border-color:   $modal-header-border-color;
    -
    -$modal-lg:                    900px;
    -$modal-md:                    600px;
    -$modal-sm:                    300px;
    -
    -
    -//== Alerts
    -//
    -//## Define alert colors, border radius, and padding.
    -
    -$alert-padding:               15px;
    -$alert-border-radius:         $border-radius-base;
    -$alert-link-font-weight:      bold;
    -
    -$alert-success-bg:            $state-success-bg;
    -$alert-success-text:          $state-success-text;
    -$alert-success-border:        $state-success-border;
    -
    -$alert-info-bg:               $state-info-bg;
    -$alert-info-text:             $state-info-text;
    -$alert-info-border:           $state-info-border;
    -
    -$alert-warning-bg:            $state-warning-bg;
    -$alert-warning-text:          $state-warning-text;
    -$alert-warning-border:        $state-warning-border;
    -
    -$alert-danger-bg:             $state-danger-bg;
    -$alert-danger-text:           $state-danger-text;
    -$alert-danger-border:         $state-danger-border;
    -
    -
    -//== Progress bars
    -//
    -//##
    -
    -//** Background color of the whole progress component
    -$progress-bg:                 #f5f5f5;
    -//** Progress bar text color
    -$progress-bar-color:          #fff;
    -//** Variable for setting rounded corners on progress bar.
    -$progress-border-radius:      $border-radius-base;
    -
    -//** Default progress bar color
    -$progress-bar-bg:             $brand-primary;
    -//** Success progress bar color
    -$progress-bar-success-bg:     $brand-success;
    -//** Warning progress bar color
    -$progress-bar-warning-bg:     $brand-warning;
    -//** Danger progress bar color
    -$progress-bar-danger-bg:      $brand-danger;
    -//** Info progress bar color
    -$progress-bar-info-bg:        $brand-info;
    -
    -
    -//== List group
    -//
    -//##
    -
    -//** Background color on `.list-group-item`
    -$list-group-bg:                 #fff;
    -//** `.list-group-item` border color
    -$list-group-border:             #ddd;
    -//** List group border radius
    -$list-group-border-radius:      $border-radius-base;
    -
    -//** Background color of single list items on hover
    -$list-group-hover-bg:           #f5f5f5;
    -//** Text color of active list items
    -$list-group-active-color:       $component-active-color;
    -//** Background color of active list items
    -$list-group-active-bg:          $component-active-bg;
    -//** Border color of active list elements
    -$list-group-active-border:      $list-group-active-bg;
    -//** Text color for content within active list items
    -$list-group-active-text-color:  lighten($list-group-active-bg, 40%);
    -
    -//** Text color of disabled list items
    -$list-group-disabled-color:      $gray-light;
    -//** Background color of disabled list items
    -$list-group-disabled-bg:         $gray-lighter;
    -//** Text color for content within disabled list items
    -$list-group-disabled-text-color: $list-group-disabled-color;
    -
    -$list-group-link-color:         #555;
    -$list-group-link-hover-color:   $list-group-link-color;
    -$list-group-link-heading-color: #333;
    -
    -
    -//== Panels
    -//
    -//##
    -
    -$panel-bg:                    #fff;
    -$panel-body-padding:          15px;
    -$panel-heading-padding:       10px 15px;
    -$panel-footer-padding:        $panel-heading-padding;
    -$panel-border-radius:         $border-radius-base;
    -
    -//** Border color for elements within panels
    -$panel-inner-border:          #ddd;
    -$panel-footer-bg:             #f5f5f5;
    -
    -$panel-default-text:          $text-color;
    -$panel-default-border:        #ddd;
    -$panel-default-heading-bg:    #f5f5f5;
    -
    -$panel-primary-text:          #fff;
    -$panel-primary-border:        $panel-default-border;
    -$panel-primary-heading-bg:    $brand-primary;
    -
    -$panel-success-text:          $state-success-text;
    -$panel-success-border:        $panel-default-border;
    -$panel-success-heading-bg:    $brand-success;
    -
    -$panel-info-text:             $state-info-text;
    -$panel-info-border:           $panel-default-border;
    -$panel-info-heading-bg:       $brand-info;
    -
    -$panel-warning-text:          $state-warning-text;
    -$panel-warning-border:        $panel-default-border;
    -$panel-warning-heading-bg:    $brand-warning;
    -
    -$panel-danger-text:           $state-danger-text;
    -$panel-danger-border:         $panel-default-border;
    -$panel-danger-heading-bg:     $brand-danger;
    -
    -
    -//== Thumbnails
    -//
    -//##
    -
    -//** Padding around the thumbnail image
    -$thumbnail-padding:           4px;
    -//** Thumbnail background color
    -$thumbnail-bg:                $body-bg;
    -//** Thumbnail border color
    -$thumbnail-border:            #ddd;
    -//** Thumbnail border radius
    -$thumbnail-border-radius:     $border-radius-base;
    -
    -//** Custom text color for thumbnail captions
    -$thumbnail-caption-color:     $text-color;
    -//** Padding around the thumbnail caption
    -$thumbnail-caption-padding:   9px;
    -
    -
    -//== Wells
    -//
    -//##
    -
    -$well-bg:                     #f5f5f5;
    -$well-border:                 darken($well-bg, 7%);
    -
    -
    -//== Badges
    -//
    -//##
    -
    -$badge-color:                 #fff;
    -//** Linked badge text color on hover
    -$badge-link-hover-color:      #fff;
    -$badge-bg:                    $brand-primary;
    -
    -//** Badge text color in active nav link
    -$badge-active-color:          $link-color;
    -//** Badge background color in active nav link
    -$badge-active-bg:             #fff;
    -
    -$badge-font-weight:           bold;
    -$badge-line-height:           1;
    -$badge-border-radius:         10px;
    -
    -
    -//== Breadcrumbs
    -//
    -//##
    -
    -$breadcrumb-padding-vertical:   8px;
    -$breadcrumb-padding-horizontal: 15px;
    -//** Breadcrumb background color
    -$breadcrumb-bg:                 #f5f5f5;
    -//** Breadcrumb text color
    -$breadcrumb-color:              #ccc;
    -//** Text color of current page in the breadcrumb
    -$breadcrumb-active-color:       $gray-light;
    -//** Textual separator for between breadcrumb elements
    -$breadcrumb-separator:          "/";
    -
    -
    -//== Carousel
    -//
    -//##
    -
    -$carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
    -
    -$carousel-control-color:                      #fff;
    -$carousel-control-width:                      15%;
    -$carousel-control-opacity:                    .5;
    -$carousel-control-font-size:                  20px;
    -
    -$carousel-indicator-active-bg:                #fff;
    -$carousel-indicator-border-color:             #fff;
    -
    -$carousel-caption-color:                      #fff;
    -
    -
    -//== Close
    -//
    -//##
    -
    -$close-font-weight:           bold;
    -$close-color:                 #000;
    -$close-text-shadow:           0 1px 0 #fff;
    -
    -
    -//== Code
    -//
    -//##
    -
    -$code-color:                  #c7254e;
    -$code-bg:                     #f9f2f4;
    -
    -$kbd-color:                   #fff;
    -$kbd-bg:                      #333;
    -
    -$pre-bg:                      #f5f5f5;
    -$pre-color:                   $gray-dark;
    -$pre-border-color:            #ccc;
    -$pre-scrollable-max-height:   340px;
    -
    -
    -//== Type
    -//
    -//##
    -
    -//** Horizontal offset for forms and lists.
    -$component-offset-horizontal: 180px;
    -//** Text muted color
    -$text-muted:                  $gray-light;
    -//** Abbreviations and acronyms border color
    -$abbr-border-color:           $gray-light;
    -//** Headings small color
    -$headings-small-color:        $gray-light;
    -//** Blockquote small color
    -$blockquote-small-color:      $gray-light;
    -//** Blockquote font size
    -$blockquote-font-size:        ($font-size-base * 1.25);
    -//** Blockquote border color
    -$blockquote-border-color:     $gray-lighter;
    -//** Page header border color
    -$page-header-border-color:    $gray-lighter;
    -//** Width of horizontal description list titles
    -$dl-horizontal-offset:        $component-offset-horizontal;
    -//** Horizontal line color.
    -$hr-border:                   $gray-lighter;
    diff --git a/app/assets/stylesheets/rails_admin/themes/default/mixins.scss b/app/assets/stylesheets/rails_admin/themes/default/mixins.scss
    deleted file mode 100644
    index 9a4c0256fb..0000000000
    --- a/app/assets/stylesheets/rails_admin/themes/default/mixins.scss
    +++ /dev/null
    @@ -1,8 +0,0 @@
    -/*
    -  Customize Sass mixins from Twitter-Bootstrap/RailsAdmin theme or add new ones for your own use.
    -
    -  Available mixins to use/override:
    -
    -  https://github.com/twbs/bootstrap-sass/tree/master/assets/stylesheets/bootstrap/mixins
    -  https://github.com/sferik/rails_admin/blob/master/app/assets/stylesheets/rails_admin/base/mixins.scss
    -*/
    diff --git a/app/assets/stylesheets/rails_admin/themes/default/theming.scss b/app/assets/stylesheets/rails_admin/themes/default/theming.scss
    deleted file mode 100644
    index ddf83a85ca..0000000000
    --- a/app/assets/stylesheets/rails_admin/themes/default/theming.scss
    +++ /dev/null
    @@ -1,11 +0,0 @@
    -/*
    -  Customize RailsAdmin theme here.
    -
    -  Look at the markup in RailsAdmin and go there to get inspiration from:
    -
    -  http://getbootstrap.com
    -
    -  Test me: (actual color should be the one defined in variables.scss if you did)
    -
    -  body { background-color: $link-color; }
    -*/
    diff --git a/app/assets/stylesheets/rails_admin/themes/default/variables.scss b/app/assets/stylesheets/rails_admin/themes/default/variables.scss
    deleted file mode 100644
    index 53c1384ac4..0000000000
    --- a/app/assets/stylesheets/rails_admin/themes/default/variables.scss
    +++ /dev/null
    @@ -1,12 +0,0 @@
    -/*
    -  Customize Sass variables from Twitter-Bootstrap/RailsAdmin theme or add new ones for your own use.
    -
    -  Available variables to use/override:
    -
    -  https://github.com/twbs/bootstrap-sass/blob/master/assets/stylesheets/bootstrap/_variables.scss
    -  https://github.com/sferik/rails_admin/blob/master/app/assets/stylesheets/rails_admin/base/variables.scss
    -
    -  Test me: pink links
    -
    -  $link-color: #F0F;
    -*/
    diff --git a/app/controllers/rails_admin/application_controller.rb b/app/controllers/rails_admin/application_controller.rb
    index 02dccc0529..8fe593742e 100644
    --- a/app/controllers/rails_admin/application_controller.rb
    +++ b/app/controllers/rails_admin/application_controller.rb
    @@ -1,3 +1,5 @@
    +# frozen_string_literal: true
    +
     require 'rails_admin/abstract_model'
     
     module RailsAdmin
    @@ -25,13 +27,14 @@ class ApplicationController < Config.parent_controller.constantize
     
         def get_model
           @model_name = to_model_name(params[:model_name])
    -      raise(RailsAdmin::ModelNotFound) unless (@abstract_model = RailsAdmin::AbstractModel.new(@model_name))
    -      raise(RailsAdmin::ModelNotFound) if (@model_config = @abstract_model.config).excluded?
    +      raise RailsAdmin::ModelNotFound unless (@abstract_model = RailsAdmin::AbstractModel.new(@model_name))
    +      raise RailsAdmin::ModelNotFound if (@model_config = @abstract_model.config).excluded?
    +
           @properties = @abstract_model.properties
         end
     
         def get_object
    -      raise(RailsAdmin::ObjectNotFound) unless (@object = @abstract_model.get(params[:id]))
    +      raise RailsAdmin::ObjectNotFound unless (@object = @abstract_model.get(params[:id], @model_config.scope))
         end
     
         def to_model_name(param)
    diff --git a/app/controllers/rails_admin/main_controller.rb b/app/controllers/rails_admin/main_controller.rb
    index 98bae9e00f..af5a69aba3 100644
    --- a/app/controllers/rails_admin/main_controller.rb
    +++ b/app/controllers/rails_admin/main_controller.rb
    @@ -1,77 +1,82 @@
    +# frozen_string_literal: true
    +
     module RailsAdmin
       class MainController < RailsAdmin::ApplicationController
         include ActionView::Helpers::TextHelper
         include RailsAdmin::MainHelper
         include RailsAdmin::ApplicationHelper
     
    -    layout :get_layout
    -
    -    before_action :get_model, except: RailsAdmin::Config::Actions.all(:root).collect(&:action_name)
    -    before_action :get_object, only: RailsAdmin::Config::Actions.all(:member).collect(&:action_name)
         before_action :check_for_cancel
     
    -    RailsAdmin::Config::Actions.all.each do |action|
    -      class_eval <<-EOS, __FILE__, __LINE__ + 1
    -        def #{action.action_name}
    -          action = RailsAdmin::Config::Actions.find('#{action.action_name}'.to_sym)
    -          @authorization_adapter.try(:authorize, action.authorization_key, @abstract_model, @object)
    -          @action = action.with({controller: self, abstract_model: @abstract_model, object: @object})
    -          fail(ActionNotAllowed) unless @action.enabled?
    -          @page_name = wording_for(:title)
    -
    -          instance_eval &@action.controller
    -        end
    -      EOS
    -    end
    -
         def bulk_action
    -      send(params[:bulk_action]) if params[:bulk_action].in?(RailsAdmin::Config::Actions.all(controller: self, abstract_model: @abstract_model).select(&:bulkable?).collect(&:route_fragment))
    +      get_model
    +      process(params[:bulk_action]) if params[:bulk_action].in?(RailsAdmin::Config::Actions.all(:bulkable, controller: self, abstract_model: @abstract_model).collect(&:route_fragment))
         end
     
         def list_entries(model_config = @model_config, auth_scope_key = :index, additional_scope = get_association_scope_from_params, pagination = !(params[:associated_collection] || params[:all] || params[:bulk_ids]))
    -      scope = model_config.abstract_model.scoped
    -      if auth_scope = @authorization_adapter && @authorization_adapter.query(auth_scope_key, model_config.abstract_model)
    -        scope = scope.merge(auth_scope)
    -      end
    +      scope = model_config.scope
    +      auth_scope = @authorization_adapter&.query(auth_scope_key, model_config.abstract_model)
    +      scope = scope.merge(auth_scope) if auth_scope
           scope = scope.instance_eval(&additional_scope) if additional_scope
           get_collection(model_config, scope, pagination)
         end
     
       private
     
    -    def get_layout
    -      "rails_admin/#{request.headers['X-PJAX'] ? 'pjax' : 'application'}"
    +    def action_missing(name, *_args)
    +      action = RailsAdmin::Config::Actions.find(name.to_sym)
    +      raise AbstractController::ActionNotFound.new("The action '#{name}' could not be found for #{self.class.name}") unless action
    +
    +      get_model unless action.root?
    +      get_object if action.member?
    +      @authorization_adapter.try(:authorize, action.authorization_key, @abstract_model, @object)
    +      @action = action.with({controller: self, abstract_model: @abstract_model, object: @object})
    +      raise(ActionNotAllowed) unless @action.enabled?
    +
    +      @page_name = wording_for(:title)
    +
    +      instance_eval(&@action.controller)
    +    end
    +
    +    def method_missing(name, *args, &block)
    +      action = RailsAdmin::Config::Actions.find(name.to_sym)
    +      if action
    +        action_missing name, *args, &block
    +      else
    +        super
    +      end
    +    end
    +
    +    def respond_to_missing?(sym, include_private)
    +      if RailsAdmin::Config::Actions.find(sym)
    +        true
    +      else
    +        super
    +      end
         end
     
         def back_or_index
    -      params[:return_to].presence && params[:return_to].include?(request.host) && (params[:return_to] != request.fullpath) ? params[:return_to] : index_path
    +      allowed_return_to?(params[:return_to].to_s) ? params[:return_to] : index_path
         end
     
    -    def get_sort_hash(model_config)
    -      abstract_model = model_config.abstract_model
    -      params[:sort] = params[:sort_reverse] = nil unless model_config.list.fields.collect { |f| f.name.to_s }.include? params[:sort]
    -      params[:sort] ||= model_config.list.sort_by.to_s
    -      params[:sort_reverse] ||= 'false'
    +    def allowed_return_to?(url)
    +      url != request.fullpath && url.start_with?(request.base_url, '/') && !url.start_with?('//')
    +    end
     
    +    def get_sort_hash(model_config)
           field = model_config.list.fields.detect { |f| f.name.to_s == params[:sort] }
    -      column = begin
    -        if field.nil? || field.sortable == true # use params[:sort] on the base table
    -          "#{abstract_model.table_name}.#{params[:sort]}"
    -        elsif field.sortable == false # use default sort, asked field is not sortable
    -          "#{abstract_model.table_name}.#{model_config.list.sort_by}"
    -        elsif (field.sortable.is_a?(String) || field.sortable.is_a?(Symbol)) && field.sortable.to_s.include?('.') # just provide sortable, don't do anything smart
    -          field.sortable
    -        elsif field.sortable.is_a?(Hash) # just join sortable hash, don't do anything smart
    -          "#{field.sortable.keys.first}.#{field.sortable.values.first}"
    -        elsif field.association? # use column on target table
    -          "#{field.associated_model_config.abstract_model.table_name}.#{field.sortable}"
    -        else # use described column in the field conf.
    -          "#{abstract_model.table_name}.#{field.sortable}"
    +      # If no sort param, default to the `sort_by` specified in the list config
    +      field ||= model_config.list.possible_fields.detect { |f| f.name == model_config.list.sort_by.try(:to_sym) }
    +
    +      column =
    +        if field.nil? || field.sortable == false # use default sort, asked field does not exist or is not sortable
    +          model_config.list.sort_by
    +        else
    +          field.sort_column
             end
    -      end
     
    -      reversed_sort = (field ? field.sort_reverse? : model_config.list.sort_reverse?)
    -      {sort: column, sort_reverse: (params[:sort_reverse] == reversed_sort.to_s)}
    +      params[:sort_reverse] ||= 'false'
    +      {sort: column, sort_reverse: (params[:sort_reverse] == (field&.sort_reverse&.to_s || 'true'))}
         end
     
         def redirect_to_on_success
    @@ -91,6 +96,7 @@ def visible_fields(action, model_config = @model_config)
     
         def sanitize_params_for!(action, model_config = @model_config, target_params = params[@abstract_model.param_key])
           return unless target_params.present?
    +
           fields = visible_fields(action, model_config)
           allowed_methods = fields.collect(&:allowed_methods).flatten.uniq.collect(&:to_s) << 'id' << '_destroy'
           fields.each { |field| field.parse_input(target_params) }
    @@ -110,20 +116,22 @@ def handle_save_error(whereto = :new)
     
           respond_to do |format|
             format.html { render whereto, status: :not_acceptable }
    -        format.js   { render whereto, layout: false, status: :not_acceptable }
    +        format.js   { render whereto, layout: 'rails_admin/modal', status: :not_acceptable, content_type: Mime[:html].to_s }
           end
         end
     
         def check_for_cancel
           return unless params[:_continue] || (params[:bulk_action] && !params[:bulk_ids])
    +
           redirect_to(back_or_index, notice: I18n.t('admin.flash.noaction'))
         end
     
         def get_collection(model_config, scope, pagination)
    -      associations = model_config.list.fields.select { |f| f.try(:eager_load?) }.collect { |f| f.association.name }
    +      section = @action.key == :export ? model_config.export : model_config.list
    +      eager_loads = section.fields.flat_map(&:eager_load_values)
           options = {}
           options = options.merge(page: (params[Kaminari.config.param_name] || 1).to_i, per: (params[:per] || model_config.list.items_per_page)) if pagination
    -      options = options.merge(include: associations) unless associations.blank?
    +      options = options.merge(include: eager_loads) unless eager_loads.blank?
           options = options.merge(get_sort_hash(model_config))
           options = options.merge(query: params[:query]) if params[:query].present?
           options = options.merge(filters: params[:f]) if params[:f].present?
    @@ -133,10 +141,11 @@ def get_collection(model_config, scope, pagination)
     
         def get_association_scope_from_params
           return nil unless params[:associated_collection].present?
    +
           source_abstract_model = RailsAdmin::AbstractModel.new(to_model_name(params[:source_abstract_model]))
           source_model_config = source_abstract_model.config
           source_object = source_abstract_model.get(params[:source_object_id])
    -      action = params[:current_action].in?(%w(create update)) ? params[:current_action] : 'edit'
    +      action = params[:current_action].in?(%w[create update]) ? params[:current_action] : 'edit'
           @association = source_model_config.send(action).fields.detect { |f| f.name == params[:associated_collection].to_sym }.with(controller: self, object: source_object)
           @association.associated_collection_scope
         end
    diff --git a/app/helpers/rails_admin/application_helper.rb b/app/helpers/rails_admin/application_helper.rb
    index 1dd8b90b7c..fbc8de8efa 100644
    --- a/app/helpers/rails_admin/application_helper.rb
    +++ b/app/helpers/rails_admin/application_helper.rb
    @@ -1,22 +1,16 @@
    -require 'rails_admin/support/i18n'
    +# frozen_string_literal: true
     
     module RailsAdmin
       module ApplicationHelper
    -    include RailsAdmin::Support::I18n
    -
    -    def capitalize_first_letter(wording)
    -      return nil unless wording.present? && wording.is_a?(String)
    -
    -      wording = wording.dup
    -      wording[0] = wording[0].mb_chars.capitalize.to_s
    -      wording
    -    end
    -
         def authorized?(action_name, abstract_model = nil, object = nil)
           object = nil if object.try :new_record?
           action(action_name, abstract_model, object).try(:authorized?)
         end
     
    +    def current_action
    +      params[:action].in?(%w[create new]) ? 'create' : 'update'
    +    end
    +
         def current_action?(action, abstract_model = @abstract_model, object = @object)
           @action.custom_key == action.custom_key &&
             abstract_model.try(:to_param) == @abstract_model.try(:to_param) &&
    @@ -32,21 +26,35 @@ def actions(scope = :all, abstract_model = nil, object = nil)
         end
     
         def edit_user_link
    -      return nil unless _current_user.respond_to?(:email)
    -      return nil unless abstract_model = RailsAdmin.config(_current_user.class).abstract_model
    -      return nil unless (edit_action = RailsAdmin::Config::Actions.find(:edit, controller: controller, abstract_model: abstract_model, object: _current_user)).try(:authorized?)
    -      link_to rails_admin.url_for(action: edit_action.action_name, model_name: abstract_model.to_param, id: _current_user.id, controller: 'rails_admin/main') do
    -        html = []
    -        html << image_tag("#{(request.ssl? ? 'https://secure' : 'http://www')}.gravatar.com/avatar/#{Digest::MD5.hexdigest _current_user.email}?s=30", alt: '') if RailsAdmin::Config.show_gravatar && _current_user.email.present?
    -        html << content_tag(:span, _current_user.email)
    -        html.join.html_safe
    +      return nil unless _current_user.try(:email).present?
    +      return nil unless (abstract_model = RailsAdmin.config(_current_user.class).abstract_model)
    +
    +      edit_action = action(:edit, abstract_model, _current_user)
    +      authorized = edit_action.try(:authorized?)
    +      content = edit_user_link_label
    +
    +      if authorized
    +        edit_url = rails_admin.url_for(
    +          action: edit_action.action_name,
    +          model_name: abstract_model.to_param,
    +          controller: 'rails_admin/main',
    +          id: _current_user.id,
    +        )
    +
    +        link_to content, edit_url, class: 'nav-link'
    +      else
    +        content_tag :span, content, class: 'nav-link'
           end
         end
     
         def logout_path
           if defined?(Devise)
             scope = Devise::Mapping.find_scope!(_current_user)
    -        main_app.send("destroy_#{scope}_session_path") rescue false
    +        begin
    +          main_app.send("destroy_#{scope}_session_path")
    +        rescue StandardError
    +          false
    +        end
           elsif main_app.respond_to?(:logout_path)
             main_app.logout_path
           end
    @@ -54,18 +62,19 @@ def logout_path
     
         def logout_method
           return [Devise.sign_out_via].flatten.first if defined?(Devise)
    +
           :delete
         end
     
         def wording_for(label, action = @action, abstract_model = @abstract_model, object = @object)
           model_config = abstract_model.try(:config)
    -      object = abstract_model && object.is_a?(abstract_model.model) ? object : nil
    +      object = nil unless abstract_model && object.is_a?(abstract_model.model)
           action = RailsAdmin::Config::Actions.find(action.to_sym) if action.is_a?(Symbol) || action.is_a?(String)
     
    -      capitalize_first_letter I18n.t(
    +      I18n.t(
             "admin.actions.#{action.i18n_key}.#{label}",
    -        model_label: model_config && model_config.label,
    -        model_label_plural: model_config && model_config.label_plural,
    +        model_label: model_config&.label,
    +        model_label_plural: model_config&.label_plural,
             object_label: model_config && object.try(model_config.object_label_method),
           )
         end
    @@ -73,52 +82,56 @@ def wording_for(label, action = @action, abstract_model = @abstract_model, objec
         def main_navigation
           nodes_stack = RailsAdmin::Config.visible_models(controller: controller)
           node_model_names = nodes_stack.collect { |c| c.abstract_model.model_name }
    +      parent_groups = nodes_stack.group_by { |n| n.parent&.to_s }
     
           nodes_stack.group_by(&:navigation_label).collect do |navigation_label, nodes|
             nodes = nodes.select { |n| n.parent.nil? || !n.parent.to_s.in?(node_model_names) }
    -        li_stack = navigation nodes_stack, nodes
    +        li_stack = navigation parent_groups, nodes
     
             label = navigation_label || t('admin.misc.navigation')
     
    -        %(#{li_stack}) if li_stack.present?
    +        collapsible_stack(label, 'main', li_stack)
           end.join.html_safe
         end
     
         def root_navigation
           actions(:root).select(&:show_in_sidebar).group_by(&:sidebar_label).collect do |label, nodes|
             li_stack = nodes.map do |node|
    -          url = rails_admin.url_for(action: node.action_name, controller: "rails_admin/main")
    +          url = rails_admin.url_for(action: node.action_name, controller: 'rails_admin/main')
               nav_icon = node.link_icon ? %().html_safe : ''
               content_tag :li do
    -            link_to nav_icon + " " + wording_for(:menu, node), url, class: "pjax"
    +            link_to nav_icon + " " + wording_for(:menu, node), url, class: "nav-link"
               end
             end.join.html_safe
             label ||= t('admin.misc.root_navigation')
     
    -        %(#{li_stack}) if li_stack.present?
    +        collapsible_stack(label, 'action', li_stack)
           end.join.html_safe
         end
     
         def static_navigation
           li_stack = RailsAdmin::Config.navigation_static_links.collect do |title, url|
    -        content_tag(:li, link_to(title.to_s, url, target: '_blank', rel: 'noopener noreferrer'))
    -      end.join
    +        content_tag(:li, link_to(title.to_s, url, target: '_blank', rel: 'noopener noreferrer', class: 'nav-link'))
    +      end.join.html_safe
     
           label = RailsAdmin::Config.navigation_static_label || t('admin.misc.navigation_static_label')
    -      li_stack = %(#{li_stack}).html_safe if li_stack.present?
    -      li_stack
    +      collapsible_stack(label, 'static', li_stack) || ''
         end
     
    -    def navigation(nodes_stack, nodes, level = 0)
    +    def navigation(parent_groups, nodes, level = 0)
           nodes.collect do |node|
    -        model_param = node.abstract_model.to_param
    +        abstract_model = node.abstract_model
    +        model_param = abstract_model.to_param
             url         = rails_admin.url_for(action: :index, controller: 'rails_admin/main', model_name: model_param)
    -        level_class = " nav-level-#{level}" if level > 0
             nav_icon = node.navigation_icon ? %().html_safe : ''
    +        css_classes = ['nav-link']
    +        css_classes.push("nav-level-#{level}") if level > 0
    +        css_classes.push('active') if @action && current_action?(@action, model_param)
             li = content_tag :li, data: {model: model_param} do
    -          link_to nav_icon + capitalize_first_letter(node.label_plural), url, class: "pjax#{level_class}"
    +          link_to nav_icon + " " + node.label_plural, url, class: css_classes.join(' ')
             end
    -        li + navigation(nodes_stack, nodes_stack.select { |n| n.parent.to_s == node.abstract_model.model_name }, level + 1)
    +        child_nodes = parent_groups[abstract_model.model_name]
    +        child_nodes ? li + navigation(parent_groups, child_nodes, level + 1) : li
           end.join.html_safe
         end
     
    @@ -131,51 +144,56 @@ def breadcrumb(action = @action, _acc = [])
             parent_actions.collect do |a|
               am = a.send(:eval, 'bindings[:abstract_model]')
               o = a.send(:eval, 'bindings[:object]')
    -          content_tag(:li, class: current_action?(a, am, o) && 'active') do
    -            crumb = begin
    -              if !current_action?(a, am, o)
    -                if a.http_methods.include?(:get)
    -                  link_to rails_admin.url_for(action: a.action_name, controller: 'rails_admin/main', model_name: am.try(:to_param), id: (o.try(:persisted?) && o.try(:id) || nil)), class: 'pjax' do
    -                    wording_for(:breadcrumb, a, am, o)
    -                  end
    -                else
    -                  content_tag(:span, wording_for(:breadcrumb, a, am, o))
    -                end
    -              else
    +          content_tag(:li, class: ['breadcrumb-item', current_action?(a, am, o) && 'active']) do
    +            if current_action?(a, am, o)
    +              wording_for(:breadcrumb, a, am, o)
    +            elsif a.http_methods.include?(:get)
    +              link_to rails_admin.url_for(action: a.action_name, controller: 'rails_admin/main', model_name: am.try(:to_param), id: (o.try(:persisted?) && o.try(:id) || nil)) do
                     wording_for(:breadcrumb, a, am, o)
                   end
    +            else
    +              content_tag(:span, wording_for(:breadcrumb, a, am, o))
                 end
    -            crumb
               end
             end.reverse.join.html_safe
           end
         end
     
         # parent => :root, :collection, :member
    -    def menu_for(parent, abstract_model = nil, object = nil, only_icon = false) # perf matters here (no action view trickery)
    +    # perf matters here (no action view trickery)
    +    def menu_for(parent, abstract_model = nil, object = nil, only_icon = false)
           actions = actions(parent, abstract_model, object).select { |a| a.http_methods.include?(:get) && a.show_in_menu }
           actions.collect do |action|
             wording = wording_for(:menu, action)
    -        %(
    -          
    -        )
    -      end.join.html_safe
    +        li_class = ['nav-item', 'icon', "#{action.key}_#{parent}_link"].
    +                   concat(action.enabled? ? [] : ['disabled'])
    +        content_tag(:li, {class: li_class}.merge(only_icon ? {title: wording, rel: 'tooltip'} : {})) do
    +          label = content_tag(:i, '', {class: action.link_icon}) + ' ' + content_tag(:span, wording, (only_icon ? {style: 'display:none'} : {}))
    +          if action.enabled? || !only_icon
    +            href =
    +              if action.enabled?
    +                rails_admin.url_for(action: action.action_name, controller: 'rails_admin/main', model_name: abstract_model.try(:to_param), id: (object.try(:persisted?) && object.try(:id) || nil))
    +              else
    +                'javascript:void(0)'
    +              end
    +            content_tag(:a, label, {href: href, target: action.link_target, class: ['nav-link', current_action?(action) && 'active', !action.enabled? && 'disabled'].compact}.merge(action.turbo? ? {} : {data: {turbo: 'false'}}))
    +          else
    +            content_tag(:span, label)
    +          end
    +        end
    +      end.join(' ').html_safe
         end
     
         def bulk_menu(abstract_model = @abstract_model)
           actions = actions(:bulkable, abstract_model)
           return '' if actions.empty?
    -      content_tag :li, class: 'dropdown', style: 'float:right' do
    -        content_tag(:a, class: 'dropdown-toggle', data: {toggle: 'dropdown'}, href: '#') { t('admin.misc.bulk_menu_title').html_safe + ' ' + ''.html_safe } +
    +
    +      content_tag :li, class: 'nav-item dropdown dropdown-menu-end' do
    +        content_tag(:a, class: 'nav-link dropdown-toggle', data: {'bs-toggle': 'dropdown'}, href: '#') { t('admin.misc.bulk_menu_title').html_safe + ' ' + ''.html_safe } +
               content_tag(:ul, class: 'dropdown-menu', style: 'left:auto; right:0;') do
                 actions.collect do |action|
                   content_tag :li do
    -                link_to wording_for(:bulk_link, action, abstract_model), '#', class: 'bulk-link', data: {action: action.action_name}
    +                link_to wording_for(:bulk_link, action, abstract_model), '#', class: 'dropdown-item bulk-link', data: {action: action.action_name}
                   end
                 end.join.html_safe
               end
    @@ -190,5 +208,57 @@ def flash_alert_class(flash_key)
           else "alert-#{flash_key}"
           end
         end
    +
    +    def handle_asset_dependency_error
    +      yield
    +    rescue LoadError => e
    +      if /sassc/.match?(e.message)
    +        e = e.exception <<~MSG
    +          #{e.message}
    +          RailsAdmin requires the gem sassc-rails, make sure to put `gem 'sassc-rails'` to Gemfile.
    +        MSG
    +      end
    +      raise e
    +    end
    +
    +    # Workaround for https://github.com/rails/rails/issues/31325
    +    def image_tag(source, options = {})
    +      if %w[ActiveStorage::Variant ActiveStorage::VariantWithRecord ActiveStorage::Preview].include? source.class.to_s
    +        super main_app.route_for(ActiveStorage.resolve_model_to_route, source), options
    +      else
    +        super
    +      end
    +    end
    +
    +  private
    +
    +    def edit_user_link_label
    +      [
    +        RailsAdmin::Config.show_gravatar &&
    +          image_tag(gravatar_url(_current_user.email), alt: ''),
    +
    +        content_tag(:span, _current_user.email),
    +      ].filter(&:present?).join.html_safe
    +    end
    +
    +    def gravatar_url(email)
    +      "https://secure.gravatar.com/avatar/#{Digest::MD5.hexdigest email}?s=30"
    +    end
    +
    +    def collapsible_stack(label, class_prefix, li_stack)
    +      return nil unless li_stack.present?
    +
    +      collapse_classname = "#{class_prefix}-#{Digest::MD5.hexdigest(label)[0..7]}"
    +      content_tag(:li, class: 'mb-1') do
    +        content_tag(:button, 'aria-expanded': true, class: 'btn btn-toggle align-items-center rounded', data: {bs_toggle: "collapse", bs_target: ".sidebar .#{collapse_classname}"}) do
    +          content_tag(:i, '', class: 'fas fa-chevron-down') + html_escape(' ' + label)
    +        end +
    +          content_tag(:div, class: "collapse show #{collapse_classname}") do
    +            content_tag(:ul, class: 'btn-toggle-nav list-unstyled fw-normal pb-1') do
    +              li_stack
    +            end
    +          end
    +      end
    +    end
       end
     end
    diff --git a/app/helpers/rails_admin/form_builder.rb b/app/helpers/rails_admin/form_builder.rb
    index facfc84ea9..d52f9eb2cf 100644
    --- a/app/helpers/rails_admin/form_builder.rb
    +++ b/app/helpers/rails_admin/form_builder.rb
    @@ -1,3 +1,7 @@
    +# frozen_string_literal: true
    +
    +require 'nested_form/builder_mixin'
    +
     module RailsAdmin
       class FormBuilder < ::ActionView::Helpers::FormBuilder
         include ::NestedForm::BuilderMixin
    @@ -30,7 +34,7 @@ def fieldset_for(fieldset, nested_in)
     
           @template.content_tag :fieldset do
             contents = []
    -        contents << @template.content_tag(:legend, %( #{fieldset.label}).html_safe, style: fieldset.name == :default ? 'display:none' : '')
    +        contents << @template.content_tag(:legend, %( #{fieldset.label}).html_safe, style: fieldset.name == :default ? 'display:none' : '')
             contents << @template.content_tag(:p, fieldset.help) if fieldset.help.present?
             contents << fields.collect { |field| field_wrapper_for(field, nested_in) }.join
             contents.join.html_safe
    @@ -40,9 +44,10 @@ def fieldset_for(fieldset, nested_in)
         def field_wrapper_for(field, nested_in)
           # do not show nested field if the target is the origin
           return if nested_field_association?(field, nested_in)
    -      @template.content_tag(:div, class: "form-group control-group #{field.type_css_class} #{field.css_class} #{'error' if field.errors.present?}", id: "#{dom_id(field)}_field") do
    +
    +      @template.content_tag(:div, class: "control-group row mb-3 #{field.type_css_class} #{field.css_class} #{'error' if field.errors.present?}", id: "#{dom_id(field)}_field") do
             if field.label
    -          label(field.method_name, capitalize_first_letter(field.label), class: 'col-sm-2 control-label') +
    +          label(field.method_name, field.label, class: 'col-sm-2 col-form-label text-md-end') +
                 (field.nested_form ? field_for(field) : input_for(field))
             else
               field.nested_form ? field_for(field) : input_for(field)
    @@ -65,7 +70,7 @@ def errors_for(field)
         end
     
         def help_for(field)
    -      field.help.present? ? @template.content_tag(:span, field.help, class: 'help-block') : ''.html_safe
    +      field.help.present? ? @template.content_tag(:div, field.help, class: 'form-text') : ''.html_safe
         end
     
         def field_for(field)
    @@ -75,13 +80,13 @@ def field_for(field)
         def object_infos
           model_config = RailsAdmin.config(object)
           model_label = model_config.label
    -      object_label = begin
    +      object_label =
             if object.new_record?
               I18n.t('admin.form.new_model', name: model_label)
             else
               object.send(model_config.object_label_method).presence || "#{model_config.label} ##{object.id}"
             end
    -      end
    +
           %().html_safe
         end
     
    @@ -102,6 +107,14 @@ def dom_name(field)
           (@dom_name ||= {})[field.name] ||= %(#{@object_name}#{options[:index] && "[#{options[:index]}]"}[#{field.method_name}]#{field.is_a?(Config::Fields::Association) && field.multiple? ? '[]' : ''})
         end
     
    +    def hidden_field(method, options = {})
    +      if method == :id && object.id.is_a?(Array)
    +        super method, {value: RailsAdmin.config.composite_keys_serializer.serialize(object.id)}
    +      else
    +        super
    +      end
    +    end
    +
       protected
     
         def generator_action(action, nested)
    diff --git a/app/helpers/rails_admin/main_helper.rb b/app/helpers/rails_admin/main_helper.rb
    index bc56285826..3f7540b872 100644
    --- a/app/helpers/rails_admin/main_helper.rb
    +++ b/app/helpers/rails_admin/main_helper.rb
    @@ -1,4 +1,4 @@
    -require 'builder'
    +# frozen_string_literal: true
     
     module RailsAdmin
       module MainHelper
    @@ -14,26 +14,8 @@ def get_indicator(percent)
           return 'info' if percent < 34     # < 1/100 of max
           return 'success' if percent < 67  # < 1/10 of max
           return 'warning' if percent < 84  # < 1/3 of max
    -      'danger'                          # > 1/3 of max
    -    end
    -
    -    def get_column_sets(properties)
    -      sets = []
    -      property_index = 0
    -      set_index = 0
     
    -      while property_index < properties.length
    -        current_set_width = 0
    -        loop do
    -          sets[set_index] ||= []
    -          sets[set_index] << properties[property_index]
    -          current_set_width += (properties[property_index].column_width || 120)
    -          property_index += 1
    -          break if current_set_width >= RailsAdmin::Config.total_columns_width || property_index >= properties.length
    -        end
    -        set_index += 1
    -      end
    -      sets
    +      'danger'                          # > 1/3 of max
         end
     
         def filterable_fields
    @@ -42,6 +24,7 @@ def filterable_fields
     
         def ordered_filters
           return @ordered_filters if @ordered_filters.present?
    +
           @index = 0
           @ordered_filters = (params[:f].try(:permit!).try(:to_h) || @model_config.list.filters).inject({}) do |memo, filter|
             field_name = filter.is_a?(Array) ? filter.first : filter
    @@ -57,29 +40,22 @@ def ordered_filters
         end
     
         def ordered_filter_options
    -      @ordered_filter_options ||= ordered_filters.map do |duplet|
    -        options = {index: duplet[0]}
    -        filter_for_field = duplet[1]
    -        filter_name = filter_for_field.keys.first
    -        filter_hash = filter_for_field.values.first
    -        unless (field = filterable_fields.find { |f| f.name == filter_name.to_sym })
    -          raise "#{filter_name} is not currently filterable; filterable fields are #{filterable_fields.map(&:name).join(', ')}"
    -        end
    -        case field.type
    -        when :enum
    -          options[:select_options] = options_for_select(field.with(object: @abstract_model.model.new).enum, filter_hash['v'])
    -        when :date, :datetime, :time
    -          options[:datetimepicker_format] = field.parser.to_momentjs
    +      if ordered_filters
    +        @ordered_filter_options ||= ordered_filters.map do |duplet|
    +          filter_for_field = duplet[1]
    +          filter_name = filter_for_field.keys.first
    +          filter_hash = filter_for_field.values.first
    +          unless (field = filterable_fields.find { |f| f.name == filter_name.to_sym }&.with({view: self}))
    +            raise "#{filter_name} is not currently filterable; filterable fields are #{filterable_fields.map(&:name).join(', ')}"
    +          end
    +
    +          field.filter_options.merge(
    +            index: duplet[0],
    +            operator: filter_hash['o'] || field.default_filter_operator,
    +            value: filter_hash['v'],
    +          )
             end
    -        options[:label] = field.label
    -        options[:name]  = field.name
    -        options[:type]  = field.type
    -        options[:value] = filter_hash['v']
    -        options[:label] = field.label
    -        options[:operator] = filter_hash['o'] || field.default_filter_operator
    -        options[:required] = field.required
    -        options
    -      end if ordered_filters
    +      end
         end
       end
     end
    diff --git a/app/views/kaminari/ra-twitter-bootstrap/_gap.html.erb b/app/views/kaminari/ra-twitter-bootstrap/_gap.html.erb
    new file mode 100644
    index 0000000000..1e69a01255
    --- /dev/null
    +++ b/app/views/kaminari/ra-twitter-bootstrap/_gap.html.erb
    @@ -0,0 +1,5 @@
    +
  • + + <%= raw(t 'admin.pagination.truncate') %> + +
  • diff --git a/app/views/kaminari/ra-twitter-bootstrap/_gap.html.haml b/app/views/kaminari/ra-twitter-bootstrap/_gap.html.haml deleted file mode 100644 index 719ad2725d..0000000000 --- a/app/views/kaminari/ra-twitter-bootstrap/_gap.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -%li.disabled - %a{href: '#'}= raw(t 'admin.pagination.truncate') diff --git a/app/views/kaminari/ra-twitter-bootstrap/_next_page.html.erb b/app/views/kaminari/ra-twitter-bootstrap/_next_page.html.erb new file mode 100644 index 0000000000..7001511b74 --- /dev/null +++ b/app/views/kaminari/ra-twitter-bootstrap/_next_page.html.erb @@ -0,0 +1,9 @@ +<% if current_page.last? %> + +<% else %> + +<% end %> diff --git a/app/views/kaminari/ra-twitter-bootstrap/_next_page.html.haml b/app/views/kaminari/ra-twitter-bootstrap/_next_page.html.haml deleted file mode 100644 index b8f93c4e73..0000000000 --- a/app/views/kaminari/ra-twitter-bootstrap/_next_page.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- if current_page.last? - %li.next.disabled= link_to raw(t 'admin.pagination.next'), '#' -- else - %li.next= link_to raw(t 'admin.pagination.next'), url, class: (remote ? 'pjax' : '') diff --git a/app/views/kaminari/ra-twitter-bootstrap/_page.html.erb b/app/views/kaminari/ra-twitter-bootstrap/_page.html.erb new file mode 100644 index 0000000000..87bc02ed92 --- /dev/null +++ b/app/views/kaminari/ra-twitter-bootstrap/_page.html.erb @@ -0,0 +1,9 @@ +<% if page.current? %> +
  • + <%= link_to page, url, class: 'page-link' %> +
  • +<% else %> +
  • + <%= link_to page, url, class: 'page-link' %> +
  • +<% end %> diff --git a/app/views/kaminari/ra-twitter-bootstrap/_page.html.haml b/app/views/kaminari/ra-twitter-bootstrap/_page.html.haml deleted file mode 100644 index a92e5af158..0000000000 --- a/app/views/kaminari/ra-twitter-bootstrap/_page.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- if page.current? - %li.active= link_to page, url, class: (remote ? 'pjax' : '') -- else - %li= link_to page, url, class: (remote ? 'pjax' : '') diff --git a/app/views/kaminari/ra-twitter-bootstrap/_paginator.html.erb b/app/views/kaminari/ra-twitter-bootstrap/_paginator.html.erb new file mode 100644 index 0000000000..63d5eeed8d --- /dev/null +++ b/app/views/kaminari/ra-twitter-bootstrap/_paginator.html.erb @@ -0,0 +1,15 @@ +<%= paginator.render do %> + +<% end %> diff --git a/app/views/kaminari/ra-twitter-bootstrap/_paginator.html.haml b/app/views/kaminari/ra-twitter-bootstrap/_paginator.html.haml deleted file mode 100644 index 47f2b27e95..0000000000 --- a/app/views/kaminari/ra-twitter-bootstrap/_paginator.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -= paginator.render do - %ul.pagination - = prev_page_tag - - each_page do |page| - - if page.left_outer? or page.right_outer? or page.inside_window? - = page_tag page - - elsif !page.was_truncated? - = gap_tag - = next_page_tag diff --git a/app/views/kaminari/ra-twitter-bootstrap/_prev_page.html.erb b/app/views/kaminari/ra-twitter-bootstrap/_prev_page.html.erb new file mode 100644 index 0000000000..6c21bd8479 --- /dev/null +++ b/app/views/kaminari/ra-twitter-bootstrap/_prev_page.html.erb @@ -0,0 +1,9 @@ +<% if current_page.first? %> + +<% else %> + +<% end %> diff --git a/app/views/kaminari/ra-twitter-bootstrap/_prev_page.html.haml b/app/views/kaminari/ra-twitter-bootstrap/_prev_page.html.haml deleted file mode 100644 index 1b7f839fb4..0000000000 --- a/app/views/kaminari/ra-twitter-bootstrap/_prev_page.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- if current_page.first? - %li.prev.disabled= link_to raw(t 'admin.pagination.previous'), '#' -- else - %li.prev= link_to raw(t 'admin.pagination.previous'), url, class: (remote ? 'pjax' : '') diff --git a/app/views/kaminari/ra-twitter-bootstrap/without_count/_next_page.html.erb b/app/views/kaminari/ra-twitter-bootstrap/without_count/_next_page.html.erb new file mode 100644 index 0000000000..7001511b74 --- /dev/null +++ b/app/views/kaminari/ra-twitter-bootstrap/without_count/_next_page.html.erb @@ -0,0 +1,9 @@ +<% if current_page.last? %> + +<% else %> + +<% end %> diff --git a/app/views/kaminari/ra-twitter-bootstrap/without_count/_next_page.html.haml b/app/views/kaminari/ra-twitter-bootstrap/without_count/_next_page.html.haml deleted file mode 100644 index b8f93c4e73..0000000000 --- a/app/views/kaminari/ra-twitter-bootstrap/without_count/_next_page.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- if current_page.last? - %li.next.disabled= link_to raw(t 'admin.pagination.next'), '#' -- else - %li.next= link_to raw(t 'admin.pagination.next'), url, class: (remote ? 'pjax' : '') diff --git a/app/views/kaminari/ra-twitter-bootstrap/without_count/_paginator.html.erb b/app/views/kaminari/ra-twitter-bootstrap/without_count/_paginator.html.erb new file mode 100644 index 0000000000..bd81b526fd --- /dev/null +++ b/app/views/kaminari/ra-twitter-bootstrap/without_count/_paginator.html.erb @@ -0,0 +1,8 @@ +<%= paginator.render do %> + +<% end %> diff --git a/app/views/kaminari/ra-twitter-bootstrap/without_count/_paginator.html.haml b/app/views/kaminari/ra-twitter-bootstrap/without_count/_paginator.html.haml deleted file mode 100644 index dc2c17d399..0000000000 --- a/app/views/kaminari/ra-twitter-bootstrap/without_count/_paginator.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -= paginator.render do - %ul.pagination - = prev_page_tag if !current_page.first? - = next_page_tag diff --git a/app/views/kaminari/ra-twitter-bootstrap/without_count/_prev_page.html.erb b/app/views/kaminari/ra-twitter-bootstrap/without_count/_prev_page.html.erb new file mode 100644 index 0000000000..6c21bd8479 --- /dev/null +++ b/app/views/kaminari/ra-twitter-bootstrap/without_count/_prev_page.html.erb @@ -0,0 +1,9 @@ +<% if current_page.first? %> + +<% else %> + +<% end %> diff --git a/app/views/kaminari/ra-twitter-bootstrap/without_count/_prev_page.html.haml b/app/views/kaminari/ra-twitter-bootstrap/without_count/_prev_page.html.haml deleted file mode 100644 index 1b7f839fb4..0000000000 --- a/app/views/kaminari/ra-twitter-bootstrap/without_count/_prev_page.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- if current_page.first? - %li.prev.disabled= link_to raw(t 'admin.pagination.previous'), '#' -- else - %li.prev= link_to raw(t 'admin.pagination.previous'), url, class: (remote ? 'pjax' : '') diff --git a/app/views/layouts/rails_admin/_head.html.erb b/app/views/layouts/rails_admin/_head.html.erb new file mode 100644 index 0000000000..d78e4366f5 --- /dev/null +++ b/app/views/layouts/rails_admin/_head.html.erb @@ -0,0 +1,31 @@ + + + + +<%= csrf_meta_tag %> +<% case RailsAdmin::config.asset_source + when :webpacker %> + <%= stylesheet_pack_tag "rails_admin", data: {'turbo-track': 'reload'} %> + <%= javascript_pack_tag "rails_admin", defer: true, data: {'turbo-track': 'reload'} %> +<% when :sprockets %> + <% handle_asset_dependency_error do %> + <%= stylesheet_link_tag "rails_admin/application.css", media: :all, data: {'turbo-track': 'reload'} %> + <%= javascript_include_tag "rails_admin/application.js", defer: true, data: {'turbo-track': 'reload'} %> + <% end %> +<% when :vite %> + <%= vite_javascript_tag "rails_admin", defer: true, data: {'turbo-track': 'reload'} %> +<% when :webpack %> + <%= stylesheet_link_tag "rails_admin.css", media: :all, data: {'turbo-track': 'reload'} %> + <%= javascript_include_tag "rails_admin.js", defer: true, data: {'turbo-track': 'reload'} %> +<% when :importmap %> + <%= stylesheet_link_tag "rails_admin.css", media: :all, data: {'turbo-track': 'reload'} %> + <%= javascript_inline_importmap_tag(RailsAdmin::Engine.importmap.to_json(resolver: self)) %> + <%= javascript_importmap_module_preload_tags(RailsAdmin::Engine.importmap) %> + <%= javascript_importmap_shim_nonce_configuration_tag if respond_to? :javascript_importmap_shim_nonce_configuration_tag %> + <%= javascript_importmap_shim_tag if respond_to? :javascript_importmap_shim_tag %> + <%= # Preload jQuery and make it global, unless jQuery UI fails to initialize + tag.script "import jQuery from 'jquery'; window.jQuery = jQuery;".html_safe, type: "module" %> + <%= javascript_import_module_tag 'rails_admin' %> +<% else + raise "Unknown asset_source: #{RailsAdmin::config.asset_source}" + end %> diff --git a/app/views/layouts/rails_admin/_head.html.haml b/app/views/layouts/rails_admin/_head.html.haml deleted file mode 100644 index 12e92abc52..0000000000 --- a/app/views/layouts/rails_admin/_head.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -%meta{content: "IE=edge", "http-equiv" => "X-UA-Compatible"} -%meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"} -%meta{content: "width=device-width, initial-scale=1", name: "viewport; charset=utf-8"} -%meta{content: "NONE,NOARCHIVE", name: "robots"} -= csrf_meta_tag -= stylesheet_link_tag "rails_admin/rails_admin.css", media: :all -= javascript_include_tag "rails_admin/rails_admin.js" diff --git a/app/views/layouts/rails_admin/_navigation.html.erb b/app/views/layouts/rails_admin/_navigation.html.erb new file mode 100644 index 0000000000..4c9c5e89fd --- /dev/null +++ b/app/views/layouts/rails_admin/_navigation.html.erb @@ -0,0 +1,17 @@ +
    + + <%= _get_plugin_name[0] || 'Rails' %> + + <%= _get_plugin_name[1] || 'Admin' %> + + + + +
    diff --git a/app/views/layouts/rails_admin/_navigation.html.haml b/app/views/layouts/rails_admin/_navigation.html.haml deleted file mode 100644 index 01c2b24a53..0000000000 --- a/app/views/layouts/rails_admin/_navigation.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -.container-fluid - .navbar-header - %button.navbar-toggle.collapsed{ type: 'button', data: { toggle: 'collapse', target: '#secondary-navigation' } } - %span.sr-only= t('admin.toggle_navigation') - %span.icon-bar - %span.icon-bar - %span.icon-bar - %a.navbar-brand.pjax{href: dashboard_path} - = _get_plugin_name[0] || 'Rails' - %small= _get_plugin_name[1] || 'Admin' - .collapse.navbar-collapse#secondary-navigation - = render partial: 'layouts/rails_admin/secondary_navigation' diff --git a/app/views/layouts/rails_admin/_secondary_navigation.html.erb b/app/views/layouts/rails_admin/_secondary_navigation.html.erb new file mode 100644 index 0000000000..24bb316d42 --- /dev/null +++ b/app/views/layouts/rails_admin/_secondary_navigation.html.erb @@ -0,0 +1,26 @@ + diff --git a/app/views/layouts/rails_admin/_secondary_navigation.html.haml b/app/views/layouts/rails_admin/_secondary_navigation.html.haml deleted file mode 100644 index 868c759507..0000000000 --- a/app/views/layouts/rails_admin/_secondary_navigation.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%ul.nav.navbar-nav.navbar-right.root_links - - actions(:root).select(&:show_in_navigation).each do |action| - %li{class: "#{action.action_name}_root_link"}= link_to wording_for(:menu, action), { action: action.action_name, controller: 'rails_admin/main' }, class: action.pjax? ? "pjax" : "" - - if main_app_root_path = (main_app.root_path rescue false) - %li= link_to t('admin.home.name'), main_app_root_path - - if _current_user - - if user_link = edit_user_link - %li.edit_user_root_link= user_link - - if logout_path.present? - %li= link_to content_tag('span', t('admin.misc.log_out'), class: 'label label-danger'), logout_path, method: logout_method diff --git a/app/views/layouts/rails_admin/_sidebar_navigation.html.erb b/app/views/layouts/rails_admin/_sidebar_navigation.html.erb new file mode 100644 index 0000000000..dfd9cc0f06 --- /dev/null +++ b/app/views/layouts/rails_admin/_sidebar_navigation.html.erb @@ -0,0 +1,5 @@ + diff --git a/app/views/layouts/rails_admin/_sidebar_navigation.html.haml b/app/views/layouts/rails_admin/_sidebar_navigation.html.haml deleted file mode 100644 index e6a67037f5..0000000000 --- a/app/views/layouts/rails_admin/_sidebar_navigation.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -%ul.nav.nav-pills.nav-stacked= main_navigation -%ul.nav.nav-pills.nav-stacked= root_navigation -%ul.nav.nav-pills.nav-stacked= static_navigation diff --git a/app/views/layouts/rails_admin/application.html.erb b/app/views/layouts/rails_admin/application.html.erb new file mode 100644 index 0000000000..513a1018b6 --- /dev/null +++ b/app/views/layouts/rails_admin/application.html.erb @@ -0,0 +1,27 @@ + + + + <%= render "layouts/rails_admin/head" %> + + +
    " id="admin-js">
    + + +
    +
    +
    + <%= render "layouts/rails_admin/sidebar_navigation" %> +
    +
    +
    + <%= render template: 'layouts/rails_admin/content' %> +
    +
    +
    +
    + + diff --git a/app/views/layouts/rails_admin/application.html.haml b/app/views/layouts/rails_admin/application.html.haml deleted file mode 100644 index 0d6a938224..0000000000 --- a/app/views/layouts/rails_admin/application.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -!!! 5 -%html{lang: I18n.locale} - %head - = render "layouts/rails_admin/head" - %body.rails_admin - #admin-js{:'data-i18n-options' => I18n.t("admin.js").to_json} - #loading.label.label-warning{style: 'display:none; position:fixed; right:20px; bottom:20px; z-index:100000'}= t('admin.loading') - %nav.navbar.navbar-default.navbar-fixed-top - = render "layouts/rails_admin/navigation" - .container-fluid - .row - .col-sm-3.col-md-2.sidebar-nav - = render "layouts/rails_admin/sidebar_navigation" - .col-sm-9.col-sm-offset-3.col-md-10.col-md-offset-2 - .content{:'data-pjax-container' => true} - = render template: 'layouts/rails_admin/pjax' diff --git a/app/views/layouts/rails_admin/content.html.erb b/app/views/layouts/rails_admin/content.html.erb new file mode 100644 index 0000000000..247a727179 --- /dev/null +++ b/app/views/layouts/rails_admin/content.html.erb @@ -0,0 +1,22 @@ + + <%= "#{@abstract_model.try(:pretty_name) || @page_name} | #{[_get_plugin_name[0] || 'Rails', _get_plugin_name[1] || 'Admin'].join(' ')}" %> + +
    +

    + <%= @page_name %> +

    +
    +<% flash && flash.each do |key, value| %> +
    + <%= value %> + +
    +<% end %> + + +<%= yield %> diff --git a/app/views/layouts/rails_admin/modal.js.erb b/app/views/layouts/rails_admin/modal.js.erb new file mode 100644 index 0000000000..22b7f3c824 --- /dev/null +++ b/app/views/layouts/rails_admin/modal.js.erb @@ -0,0 +1,7 @@ +<% flash && flash.each do |key, value| %> +
    + <%= value %> + +
    +<% end %> +<%= yield %> diff --git a/app/views/layouts/rails_admin/pjax.html.haml b/app/views/layouts/rails_admin/pjax.html.haml deleted file mode 100644 index 4edbc350ec..0000000000 --- a/app/views/layouts/rails_admin/pjax.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%title= "#{@abstract_model.try(:pretty_name) || @page_name} | #{[_get_plugin_name[0] || 'Rails', _get_plugin_name[1] || 'Admin'].join(' ')}" -.page-header{data: {model: @abstract_model.to_param}} - %h1= @page_name -- flash && flash.each do |key, value| - .alert.alert-dismissible{class: flash_alert_class(key)} - %button.close{type: 'button', :'data-dismiss' => "alert"} × - = value -= breadcrumb -%ul.nav.nav-tabs - = menu_for((@abstract_model ? (@object.try(:persisted?) ? :member : :collection) : :root), @abstract_model, @object) - = content_for :contextual_tabs -= yield diff --git a/app/views/rails_admin/main/_dashboard_history.html.erb b/app/views/rails_admin/main/_dashboard_history.html.erb new file mode 100644 index 0000000000..e46a2dc733 --- /dev/null +++ b/app/views/rails_admin/main/_dashboard_history.html.erb @@ -0,0 +1,45 @@ + + + + + + + + + + <% @history.each do |t| %> + <% abstract_model = RailsAdmin.config(t.table).abstract_model %> + + + <% if o = abstract_model.try(:get, t.item) %> + <% label = o.send(abstract_model.config.object_label_method) %> + <% if show_action = action(:show, abstract_model, o) %> + + <% else %> + + <% end %> + <% else %> + <% label = Object.const_defined?(t.table) ? t.table.constantize.model_name.human : t.table %> + + <% end %> + + + <% end %> + +
    + <%= t("admin.table_headers.username") %> + + <%= t("admin.table_headers.item") %> + + <%= t("admin.table_headers.changes") %> +
    + <%= t.try :username %> + + <%= link_to(label, url_for(action: show_action.action_name, model_name: abstract_model.to_param, id: o.id)) %> + + <%= label %> + + <%= "#{label} ##{t.item}" %> + + <%= t.message %> +
    diff --git a/app/views/rails_admin/main/_dashboard_history.html.haml b/app/views/rails_admin/main/_dashboard_history.html.haml deleted file mode 100644 index 502696a887..0000000000 --- a/app/views/rails_admin/main/_dashboard_history.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -%table.table.table-condensed.table-striped - %thead - %tr - %th.shrink.user= t("admin.table_headers.username") - %th.shrink.items= t("admin.table_headers.item") - %th.changes= t("admin.table_headers.changes") - %tbody - - @history.each do |t| - - abstract_model = RailsAdmin.config(t.table).abstract_model - %tr - %td= t.try :username - - if o = abstract_model.try(:get, t.item) - - label = o.send(abstract_model.config.object_label_method) - - if show_action = action(:show, abstract_model, o) - %td= link_to(label, url_for(action: show_action.action_name, model_name: abstract_model.to_param, id: o.id), class: 'pjax') - - else - %td= label - - else - - label = Object.const_defined?(t.table) ? t.table.constantize.model_name.human : t.table - %td= "#{label} ##{t.item}" - %td= t.message diff --git a/app/views/rails_admin/main/_delete_notice.html.erb b/app/views/rails_admin/main/_delete_notice.html.erb new file mode 100644 index 0000000000..b20850158d --- /dev/null +++ b/app/views/rails_admin/main/_delete_notice.html.erb @@ -0,0 +1,35 @@ +<% object = delete_notice %> +
  • + + <%= @abstract_model.pretty_name %> + + <% wording = object.send(@model_config.object_label_method) %> + <% if show_action = action(:show, @abstract_model, object) %> + <%= link_to(wording, url_for(action: show_action.action_name, model_name: @abstract_model.to_param, id: object.id)) %> + <% else %> + <%= wording %> + <% end %> +
      + <% @abstract_model.each_associated_children(object) do |association, children| %> + <% humanized_association = @abstract_model.model.human_attribute_name association.name %> + <% limit = children.count > 12 ? 10 : children.count %> + <% children.first(limit).each do |child| %> + <%= content_tag :li, class: dom_class(child) do %> + <% child_config = RailsAdmin.config(child) %> + <%= humanized_association.singularize %> + <% wording = child.send(child_config.object_label_method) %> + <% if child.id && (show_action = action(:show, child_config.abstract_model, child)) %> + <%= link_to(wording, url_for(action: show_action.action_name, model_name: child_config.abstract_model.to_param, id: child.id)) %> + <% else %> + <%= wording %> + <% end %> + <% end %> + <% end %> + <% if children.count > limit %> +
    • + <%= t('admin.misc.more', count: children.count - limit, models_name: humanized_association) %> +
    • + <% end %> + <% end %> +
    +
  • diff --git a/app/views/rails_admin/main/_delete_notice.html.haml b/app/views/rails_admin/main/_delete_notice.html.haml deleted file mode 100644 index da1facfba7..0000000000 --- a/app/views/rails_admin/main/_delete_notice.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -- object = delete_notice - -%li{style: 'display:block; margin-top:10px'} - %span.label.label-default= @abstract_model.pretty_name - - wording = object.send(@model_config.object_label_method) - - if show_action = action(:show, @abstract_model, object) - = link_to(wording, url_for(action: show_action.action_name, model_name: @abstract_model.to_param, id: object.id), class: 'pjax') - - else - = wording - %ul - - @abstract_model.each_associated_children(object) do |association, children| - - humanized_association = @abstract_model.model.human_attribute_name association.name - - limit = children.count > 12 ? 10 : children.count - - children.first(limit).each do |child| - = content_tag :li, class: dom_class(child) do - - child_config = RailsAdmin.config(child) - = humanized_association.singularize - - wording = child.send(child_config.object_label_method) - - if child.id && (show_action = action(:show, child_config.abstract_model, child)) - = link_to(wording, url_for(action: show_action.action_name, model_name: child_config.abstract_model.to_param, id: child.id), class: 'pjax') - - else - = wording - - if children.count > limit - %li= t('admin.misc.more', count: children.count - limit, models_name: humanized_association) diff --git a/app/views/rails_admin/main/_form_action_text.html.erb b/app/views/rails_admin/main/_form_action_text.html.erb new file mode 100644 index 0000000000..b002b1fbb0 --- /dev/null +++ b/app/views/rails_admin/main/_form_action_text.html.erb @@ -0,0 +1,8 @@ +<% + js_data = { + csspath: field.css_location, + jspath: field.js_location, + warn_dynamic_load: field.warn_dynamic_load + } +%> +<%= form.rich_text_area field.method_name, field.html_attributes.reverse_merge(data: { options: js_data.to_json }) %> diff --git a/app/views/rails_admin/main/_form_action_text.html.haml b/app/views/rails_admin/main/_form_action_text.html.haml deleted file mode 100644 index f3a9ac4ec7..0000000000 --- a/app/views/rails_admin/main/_form_action_text.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -:ruby - js_data = { - csspath: field.css_location, - jspath: field.js_location - } - -= form.rich_text_area field.method_name, field.html_attributes.reverse_merge(data: { options: js_data.to_json }) diff --git a/app/views/rails_admin/main/_form_boolean.html.erb b/app/views/rails_admin/main/_form_boolean.html.erb new file mode 100644 index 0000000000..36a5196716 --- /dev/null +++ b/app/views/rails_admin/main/_form_boolean.html.erb @@ -0,0 +1,14 @@ +<% if field.nullable? %> +
    + <% {'1': [true, 'btn-outline-success'], '0': [false, 'btn-outline-danger'], '': [nil, 'btn-outline-secondary']}.each do |text, (value, btn_class)| %> + <%= form.radio_button field.method_name, text, field.html_attributes.reverse_merge({ checked: field.form_value == value, required: field.required, class: 'btn-check' }) %> + <%= form.label "#{field.method_name}_#{text}", class: "#{field.css_classes[value]} btn #{btn_class}" do %> + <%= field.labels[value].html_safe %> + <% end %> + <% end %> +
    +<% else %> +
    + <%= form.send field.view_helper, field.method_name, field.html_attributes.reverse_merge({ value: field.form_value, checked: field.form_value.in?([true, '1']), required: field.required, class: 'form-check-input' }) %> +
    +<% end %> diff --git a/app/views/rails_admin/main/_form_boolean.html.haml b/app/views/rails_admin/main/_form_boolean.html.haml deleted file mode 100644 index fb2550b160..0000000000 --- a/app/views/rails_admin/main/_form_boolean.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -.checkbox - %label{ style: 'display: block;' } - = form.send field.view_helper, field.method_name, field.html_attributes.reverse_merge({ value: field.form_value, checked: field.form_value.in?([true, '1']), required: field.required}) diff --git a/app/views/rails_admin/main/_form_ck_editor.html.haml b/app/views/rails_admin/main/_form_ck_editor.html.erb similarity index 57% rename from app/views/rails_admin/main/_form_ck_editor.html.haml rename to app/views/rails_admin/main/_form_ck_editor.html.erb index f5fa2a8d39..61fd226ff9 100644 --- a/app/views/rails_admin/main/_form_ck_editor.html.haml +++ b/app/views/rails_admin/main/_form_ck_editor.html.erb @@ -1,4 +1,4 @@ -:ruby +<% js_data = { jspath: field.location ? field.location : field.base_location + "ckeditor.js", base_location: field.base_location, @@ -6,5 +6,5 @@ customConfig: field.config_js ? field.config_js : field.base_location + "config.js" } } - -= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: 'ckeditor', options: js_data.to_json }).reverse_merge({ value: field.form_value }) +%> +<%= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: 'ckeditor', options: js_data.to_json }).reverse_merge({ value: field.form_value }) %> diff --git a/app/views/rails_admin/main/_form_code_mirror.html.erb b/app/views/rails_admin/main/_form_code_mirror.html.erb new file mode 100644 index 0000000000..e18cf862d8 --- /dev/null +++ b/app/views/rails_admin/main/_form_code_mirror.html.erb @@ -0,0 +1,9 @@ +<% + js_data = { + csspath: field.css_location, + jspath: field.js_location, + options: field.config, + locations: field.assets + } +%> +<%= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: 'codemirror', options: js_data.to_json }).reverse_merge({ value: field.form_value }) %> diff --git a/app/views/rails_admin/main/_form_code_mirror.html.haml b/app/views/rails_admin/main/_form_code_mirror.html.haml deleted file mode 100644 index 8b07c8d16b..0000000000 --- a/app/views/rails_admin/main/_form_code_mirror.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -:ruby - js_data = { - csspath: field.css_location, - jspath: field.js_location, - options: field.config, - locations: field.assets - } - -= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: 'codemirror', options: js_data.to_json }).reverse_merge({ value: field.form_value }) diff --git a/app/views/rails_admin/main/_form_colorpicker.html.erb b/app/views/rails_admin/main/_form_colorpicker.html.erb new file mode 100644 index 0000000000..0364f8a1ab --- /dev/null +++ b/app/views/rails_admin/main/_form_colorpicker.html.erb @@ -0,0 +1,5 @@ +
    +
    + <%= form.send field.view_helper, field.method_name, field.html_attributes.reverse_merge({class: 'form-control', value: field.form_value}) %> +
    +
    diff --git a/app/views/rails_admin/main/_form_colorpicker.html.haml b/app/views/rails_admin/main/_form_colorpicker.html.haml deleted file mode 100644 index 3f4e876f31..0000000000 --- a/app/views/rails_admin/main/_form_colorpicker.html.haml +++ /dev/null @@ -1 +0,0 @@ -= form.send field.view_helper, field.method_name, field.html_attributes.reverse_merge({class: 'form-control', value: field.form_value, style: "background-color: #{field.color}", data: {color: true}}) diff --git a/app/views/rails_admin/main/_form_datetime.html.erb b/app/views/rails_admin/main/_form_datetime.html.erb new file mode 100644 index 0000000000..51fd351e2f --- /dev/null +++ b/app/views/rails_admin/main/_form_datetime.html.erb @@ -0,0 +1,10 @@ +
    +
    +
    + <%= form.text_field field.method_name, field.html_attributes.reverse_merge({autocomplete: 'off', class: 'form-control', data: {datetimepicker: true, options: field.datepicker_options.to_json}, value: field.form_value}) %> + <%= form.label(field.method_name, class: 'input-group-text') do %> + + <% end %> +
    +
    +
    diff --git a/app/views/rails_admin/main/_form_datetime.html.haml b/app/views/rails_admin/main/_form_datetime.html.haml deleted file mode 100644 index 558c2c7fee..0000000000 --- a/app/views/rails_admin/main/_form_datetime.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -.form-inline - .input-group - = form.send field.view_helper, field.method_name, field.html_attributes.reverse_merge({value: field.form_value, class: 'form-control', data: {datetimepicker: true, options: field.datepicker_options.to_json}}) - = form.label(field.method_name, class: 'input-group-addon') do - %i.fa.fa-fw.fa-calendar diff --git a/app/views/rails_admin/main/_form_enumeration.html.erb b/app/views/rails_admin/main/_form_enumeration.html.erb new file mode 100644 index 0000000000..7bf62a1d29 --- /dev/null +++ b/app/views/rails_admin/main/_form_enumeration.html.erb @@ -0,0 +1,25 @@ +<% unless field.multiple? %> +
    +
    + <%= form.select field.method_name, field.enum, { include_blank: true }.reverse_merge({ selected: field.form_value }), field.html_attributes.reverse_merge({ data: { enumeration: true }, placeholder: t('admin.misc.search') }) %> +
    +
    +<% else %> + <% + js_data = { + xhr: false, + sortable: false, + cacheAll: true, + regional: { + add: t("admin.misc.add_new"), + chooseAll: t("admin.misc.chose_all"), + clearAll: t("admin.misc.clear_all"), + down: t("admin.misc.down"), + remove: t("admin.misc.remove"), + search: t("admin.misc.search"), + up: t("admin.misc.up") + } + } + %> + <%= form.select field.method_name, field.enum, { selected: field.form_value, object: form.object }, field.html_attributes.reverse_merge({data: { filteringmultiselect: true, options: js_data.to_json }, multiple: true}) %> +<% end %> diff --git a/app/views/rails_admin/main/_form_enumeration.html.haml b/app/views/rails_admin/main/_form_enumeration.html.haml deleted file mode 100644 index 3ebe30f4e2..0000000000 --- a/app/views/rails_admin/main/_form_enumeration.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -- unless field.multiple? - = form.select field.method_name, field.enum, { include_blank: true }.reverse_merge({ selected: field.form_value }), field.html_attributes.reverse_merge({ data: { enumeration: true }, placeholder: t('admin.misc.search') }) -- else - :ruby - js_data = { - xhr: false, - sortable: false, - cacheAll: true, - regional: { - add: t("admin.misc.add_new"), - chooseAll: t("admin.misc.chose_all"), - clearAll: t("admin.misc.clear_all"), - down: t("admin.misc.down"), - remove: t("admin.misc.remove"), - search: t("admin.misc.search"), - up: t("admin.misc.up") - } - } - = form.select field.method_name, field.enum, { selected: field.form_value, object: form.object }, field.html_attributes.reverse_merge({data: { filteringmultiselect: true, options: js_data.to_json }, multiple: true}) diff --git a/app/views/rails_admin/main/_form_field.html.erb b/app/views/rails_admin/main/_form_field.html.erb new file mode 100644 index 0000000000..8115e24d8a --- /dev/null +++ b/app/views/rails_admin/main/_form_field.html.erb @@ -0,0 +1 @@ +<%= form.send field.view_helper, field.method_name, field.html_attributes.reverse_merge({ value: field.form_value, class: 'form-control', required: field.required}) %> diff --git a/app/views/rails_admin/main/_form_field.html.haml b/app/views/rails_admin/main/_form_field.html.haml deleted file mode 100644 index 205da46a82..0000000000 --- a/app/views/rails_admin/main/_form_field.html.haml +++ /dev/null @@ -1 +0,0 @@ -= form.send field.view_helper, field.method_name, field.html_attributes.reverse_merge({ value: field.form_value, class: 'form-control', required: field.required}) diff --git a/app/views/rails_admin/main/_form_file_upload.html.erb b/app/views/rails_admin/main/_form_file_upload.html.erb new file mode 100644 index 0000000000..bc72a5e5ad --- /dev/null +++ b/app/views/rails_admin/main/_form_file_upload.html.erb @@ -0,0 +1,17 @@ +<% file = field.value %> +<% if field.cache_method %> + <%= form.hidden_field(field.cache_method, value: field.cache_value) %> +<% end %> +
    + <% if value = field.pretty_value %> + <%= value %> + <% end %> + <%= form.file_field(field.name, {data: {fileupload: true}}.deep_merge(field.html_attributes)) %> +
    +<% if field.optional? && field.errors.blank? && file && field.delete_method %> + + + <%= I18n.t('admin.actions.delete.link', object_label: field.label) %> + + <%= form.check_box(field.delete_method, style: 'display:none;') %> +<% end %> diff --git a/app/views/rails_admin/main/_form_file_upload.html.haml b/app/views/rails_admin/main/_form_file_upload.html.haml deleted file mode 100644 index 371e77e869..0000000000 --- a/app/views/rails_admin/main/_form_file_upload.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- file = field.value - -- if field.cache_method - = form.hidden_field(field.cache_method, value: field.cache_value) - -.toggle{style: ('display:none;' if file && field.delete_method && form.object.send(field.delete_method) == '1')} - - if value = field.pretty_value - = value - - = form.file_field(field.name, field.html_attributes.reverse_merge({ data: { fileupload: true }})) - -- if field.optional? && field.errors.blank? && file && field.delete_method - %a.btn.btn-info.btn-remove-image{href: '#', :'data-toggle' => 'button', role: 'button'} - %i.icon-white.icon-trash - = I18n.t('admin.actions.delete.menu').capitalize + " #{field.label.downcase}" - - = form.check_box(field.delete_method, style: 'display:none;') diff --git a/app/views/rails_admin/main/_form_filtering_multiselect.html.erb b/app/views/rails_admin/main/_form_filtering_multiselect.html.erb new file mode 100644 index 0000000000..5260087bd3 --- /dev/null +++ b/app/views/rails_admin/main/_form_filtering_multiselect.html.erb @@ -0,0 +1,19 @@ +<% + config = field.associated_model_config +%> + +
    +
    + + <%= + form.select field.method_name, field.collection, { selected: field.form_value, object: form.object }, + field.html_attributes.reverse_merge({data: { filteringmultiselect: true, options: field.widget_options.to_json }, multiple: true}) + %> +
    + <% if authorized?(:new, config.abstract_model) && field.inline_add %> + + <% end %> +
    \ No newline at end of file diff --git a/app/views/rails_admin/main/_form_filtering_multiselect.html.haml b/app/views/rails_admin/main/_form_filtering_multiselect.html.haml deleted file mode 100644 index 21e686121f..0000000000 --- a/app/views/rails_admin/main/_form_filtering_multiselect.html.haml +++ /dev/null @@ -1,49 +0,0 @@ -:ruby - related_id = params[:associations] && params[:associations][field.name.to_s] - config = field.associated_model_config - source_abstract_model = RailsAdmin.config(form.object.class).abstract_model - - if form.object.new_record? && related_id.present? && related_id != 'new' - selected = [config.abstract_model.get(related_id)] - else - selected = form.object.send(field.name) - end - selected_ids = selected.map{|s| s.send(field.associated_primary_key)} - - current_action = params[:action].in?(['create', 'new']) ? 'create' : 'update' - - xhr = !field.associated_collection_cache_all - - collection = if xhr - selected.map { |o| [o.send(field.associated_object_label_method), o.send(field.associated_primary_key)] } - else - i = 0 - controller.list_entries(config, :index, field.associated_collection_scope, false).map { |o| [o.send(field.associated_object_label_method), o.send(field.associated_primary_key)] }.sort_by {|a| [selected_ids.index(a[1]) || selected_ids.size, i+=1] } - end - - js_data = { - xhr: xhr, - :'edit-url' => (authorized?(:edit, config.abstract_model) ? edit_path(model_name: config.abstract_model.to_param, id: '__ID__') : ''), - remote_source: index_path(config.abstract_model, source_object_id: form.object.id, source_abstract_model: source_abstract_model.to_param, associated_collection: field.name, current_action: current_action, compact: true), - sortable: !!field.orderable, - removable: !!field.removable, - cacheAll: !!field.associated_collection_cache_all, - regional: { - add: t('admin.misc.add_new'), - chooseAll: t('admin.misc.chose_all'), - clearAll: t('admin.misc.clear_all'), - down: t('admin.misc.down'), - remove: t('admin.misc.remove'), - search: t('admin.misc.search'), - up: t('admin.misc.up') - } - } - -%input{name: form.dom_name(field), type: "hidden", value: ""} - -- selected_ids = (hdv = field.form_default_value).nil? ? selected_ids : hdv -= form.select field.method_name, collection, { selected: selected_ids, object: form.object }, field.html_attributes.reverse_merge({data: { filteringmultiselect: true, options: js_data.to_json }, multiple: true}) -- if authorized?(:new, config.abstract_model) && field.inline_add - - path_hash = { model_name: config.abstract_model.to_param, modal: true } - - path_hash.merge!({ associations: { field.inverse_of => (form.object.persisted? ? form.object.id : 'new') } }) if field.inverse_of - = link_to " ".html_safe + wording_for(:link, :new, config.abstract_model), '#', data: { link: new_path(path_hash) }, class: "create btn btn-info", style: 'margin-left:10px' diff --git a/app/views/rails_admin/main/_form_filtering_select.html.erb b/app/views/rails_admin/main/_form_filtering_select.html.erb new file mode 100644 index 0000000000..e527b3a9d8 --- /dev/null +++ b/app/views/rails_admin/main/_form_filtering_select.html.erb @@ -0,0 +1,21 @@ +<% + config = field.associated_model_config +%> + +
    +
    + <%= + form.select field.method_name, field.collection, { selected: field.form_value, include_blank: true }, + field.html_attributes.reverse_merge({ data: { filteringselect: true, options: field.widget_options.to_json }, placeholder: t('admin.misc.search') }) + %> +
    + +
    \ No newline at end of file diff --git a/app/views/rails_admin/main/_form_filtering_select.html.haml b/app/views/rails_admin/main/_form_filtering_select.html.haml deleted file mode 100644 index 31cb338493..0000000000 --- a/app/views/rails_admin/main/_form_filtering_select.html.haml +++ /dev/null @@ -1,37 +0,0 @@ -:ruby - config = field.associated_model_config - related_id = params[:associations] && params[:associations][field.name.to_s] - source_abstract_model = RailsAdmin.config(form.object.class).abstract_model - - if form.object.new_record? && related_id.present? && related_id != 'new' - selected = config.abstract_model.get(related_id) - selected_id = selected.send(field.associated_primary_key) - selected_name = selected.send(field.associated_object_label_method) - else - selected_id = field.selected_id - selected_name = field.formatted_value - end - - current_action = params[:action].in?(['create', 'new']) ? 'create' : 'update' - - edit_url = authorized?(:edit, config.abstract_model) ? edit_path(model_name: config.abstract_model.to_param, modal: true, id: '__ID__') : '' - - xhr = !field.associated_collection_cache_all - - collection = xhr ? [[selected_name, selected_id]] : controller.list_entries(config, :index, field.associated_collection_scope, false).map { |o| [o.send(field.associated_object_label_method), o.send(field.associated_primary_key)] } - - js_data = { - xhr: xhr, - remote_source: index_path(config.abstract_model.to_param, source_object_id: form.object.id, source_abstract_model: source_abstract_model.to_param, associated_collection: field.name, current_action: current_action, compact: true) - } - -- selected_id = (hdv = field.form_default_value).nil? ? selected_id : hdv -= form.select field.method_name, collection, { selected: selected_id, include_blank: true }, field.html_attributes.reverse_merge({ data: { filteringselect: true, options: js_data.to_json }, placeholder: t('admin.misc.search'), style: "float: left" }) - -- if authorized?(:new, config.abstract_model) && field.inline_add - - path_hash = { model_name: config.abstract_model.to_param, modal: true } - - path_hash.merge!({ associations: { field.inverse_of => (form.object.persisted? ? form.object.id : 'new') } }) if field.inverse_of - = link_to " ".html_safe + wording_for(:link, :new, config.abstract_model), '#', data: { link: new_path(path_hash) }, class: "btn btn-info create", style: 'margin-left:10px' - -- if edit_url.present? && field.inline_edit - = link_to " ".html_safe + wording_for(:link, :edit, config.abstract_model), '#', data: { link: edit_url }, class: "btn btn-info update #{field.value.nil? && 'disabled'}", style: 'margin-left:10px' diff --git a/app/views/rails_admin/main/_form_froala.html.erb b/app/views/rails_admin/main/_form_froala.html.erb new file mode 100644 index 0000000000..4107677637 --- /dev/null +++ b/app/views/rails_admin/main/_form_froala.html.erb @@ -0,0 +1,8 @@ +<% + js_data = { + csspath: field.css_location, + jspath: field.js_location, + config_options: field.config_options.to_json + } +%> +<%= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: 'froala-wysiwyg', options: js_data.to_json }).reverse_merge({ value: field.form_value }) %> diff --git a/app/views/rails_admin/main/_form_froala.html.haml b/app/views/rails_admin/main/_form_froala.html.haml deleted file mode 100644 index 7ad8570c0d..0000000000 --- a/app/views/rails_admin/main/_form_froala.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -:ruby - js_data = { - csspath: field.css_location, - jspath: field.js_location, - config_options: field.config_options.to_json - } - -= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: 'froala-wysiwyg', options: js_data.to_json }).reverse_merge({ value: field.form_value }) diff --git a/app/views/rails_admin/main/_form_multiple_file_upload.html.erb b/app/views/rails_admin/main/_form_multiple_file_upload.html.erb new file mode 100644 index 0000000000..593a47f9c4 --- /dev/null +++ b/app/views/rails_admin/main/_form_multiple_file_upload.html.erb @@ -0,0 +1,20 @@ +<% field.attachments.each_with_index do |attachment, i| %> +
    + <%= attachment.pretty_value %> + <% if field.delete_method || field.keep_method %> + + + <%= I18n.t('admin.form.delete_file', field_label: field.label, number: i + 1) %> + + <% if field.keep_method %> + <%= form.check_box(field.keep_method, {multiple:true, checked: true, style: 'display:none;'}, attachment.keep_value, nil) %> + <% elsif field.delete_method %> + <%= form.check_box(field.delete_method, {multiple:true, style: 'display:none;'}, attachment.delete_value, nil) %> + <% end %> + <% end %> +
    +<% end %> +<%= form.file_field(field.name, { data: { :"multiple-fileupload" => true }, multiple: true }.deep_merge(field.html_attributes)) %> +<% if field.cache_method %> + <%= form.hidden_field(field.cache_method) %> +<% end %> diff --git a/app/views/rails_admin/main/_form_multiple_file_upload.html.haml b/app/views/rails_admin/main/_form_multiple_file_upload.html.haml deleted file mode 100644 index 61d6c7aac5..0000000000 --- a/app/views/rails_admin/main/_form_multiple_file_upload.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- field.attachments.each_with_index do |attachment, i| - .toggle{class: field.reorderable? ? 'sortables' : ''} - = attachment.pretty_value - - if field.delete_method || field.keep_method - %a.btn.btn-info.btn-remove-image{href: '#', :'data-toggle' => 'button', role: 'button'} - %i.icon-white.icon-trash - = I18n.t('admin.actions.delete.menu').capitalize + " #{field.label.downcase} ##{i + 1}" - - if field.keep_method - = form.check_box(field.keep_method, {multiple:true, checked: true, style: 'display:none;'}, attachment.keep_value, nil) - - elsif field.delete_method - = form.check_box(field.delete_method, {multiple:true, style: 'display:none;'}, attachment.delete_value, nil) - -= form.file_field(field.name, field.html_attributes.reverse_merge({ data: { :"multiple-fileupload" => true }, multiple: true })) - -- if field.cache_method - = form.hidden_field(field.cache_method) diff --git a/app/views/rails_admin/main/_form_nested_many.html.erb b/app/views/rails_admin/main/_form_nested_many.html.erb new file mode 100644 index 0000000000..7a92f1bd97 --- /dev/null +++ b/app/views/rails_admin/main/_form_nested_many.html.erb @@ -0,0 +1,21 @@ +
    +
    + + + + <% if field.inline_add %> + <%= form.link_to_add " #{wording_for(:link, :new, field.associated_model_config.abstract_model)}".html_safe, field.name, { class: 'btn btn-info' } %> + <% end %> +
    + <%= form.errors_for(field) %> + <%= form.help_for(field) %> + +
    +
    + <%= form.fields_for field.name do |nested_form| %> + <% if field.nested_form[:allow_destroy] || nested_form.options[:child_index] == "new_#{field.name}" %> + <%= nested_form.link_to_remove ''.html_safe %> + <% end %> + <%= nested_form.generate({action: :nested, model_config: field.associated_model_config, nested_in: field }) %> + <% end %> +
    diff --git a/app/views/rails_admin/main/_form_nested_many.html.haml b/app/views/rails_admin/main/_form_nested_many.html.haml deleted file mode 100644 index 3a260ce4c6..0000000000 --- a/app/views/rails_admin/main/_form_nested_many.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -.controls.col-sm-10{data: { nestedmany: true }} - .btn-group - %a.btn.btn-info.toggler{:'data-toggle' => "button", :'data-target' => "#{form.jquery_namespace(field)} > .tab-content, #{form.jquery_namespace(field)} > .controls > .nav", class: (field.active? ? 'active' : '')} - %i.icon-white - - unless field.nested_form[:update_only] || !field.inline_add - = form.link_to_add " #{wording_for(:link, :new, field.associated_model_config.abstract_model)}".html_safe, field.name, { class: 'btn btn-info' } - = form.errors_for(field) - = form.help_for(field) - %ul.nav.nav-tabs - -.tab-content - = form.fields_for field.name do |nested_form| - - if field.nested_form[:allow_destroy] || nested_form.options[:child_index] == "new_#{field.name}" - = nested_form.link_to_remove ''.html_safe - = nested_form.generate({action: :nested, model_config: field.associated_model_config, nested_in: field }) diff --git a/app/views/rails_admin/main/_form_nested_one.html.erb b/app/views/rails_admin/main/_form_nested_one.html.erb new file mode 100644 index 0000000000..54906808de --- /dev/null +++ b/app/views/rails_admin/main/_form_nested_one.html.erb @@ -0,0 +1,21 @@ +
    + +
    + + + + <% if field.inline_add %> + <%= form.link_to_add " #{wording_for(:link, :new, field.associated_model_config.abstract_model)}".html_safe, field.name, { class: 'btn btn-info', :'data-add-label' => " #{wording_for(:link, :new, field.associated_model_config.abstract_model)}".gsub("\n", "") } %> + <% end %> +
    + <%= form.errors_for(field) %> + <%= form.help_for(field) %> +
    +
    + <%= form.fields_for field.name do |nested_form| %> + <% if field.nested_form[:allow_destroy] %> + <%= nested_form.link_to_remove ''.html_safe %> + <% end %> + <%= nested_form.generate({action: :nested, model_config: field.associated_model_config, nested_in: field }) %> + <% end %> +
    diff --git a/app/views/rails_admin/main/_form_nested_one.html.haml b/app/views/rails_admin/main/_form_nested_one.html.haml deleted file mode 100644 index 44c6847580..0000000000 --- a/app/views/rails_admin/main/_form_nested_one.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -.controls.col-sm-10{data: { nestedone: true }} - %ul.nav{style: 'margin-bottom:0px; display:none'} - .btn-group - %a.btn.btn-info.toggler{:'data-toggle' => "button", :'data-target' => "#{form.jquery_namespace(field)} > .tab-content", class: (field.active? ? 'active' : '') } - %i.icon-white - - unless field.nested_form[:update_only] || !field.inline_add - = form.link_to_add " #{wording_for(:link, :new, field.associated_model_config.abstract_model)}".html_safe, - field.name, - { class: 'btn btn-info', :'data-add-label' => " #{wording_for(:link, :new, field.associated_model_config.abstract_model)}".gsub("\n", "") } - = form.errors_for(field) - = form.help_for(field) - -.tab-content - = form.fields_for field.name do |nested_form| - - if field.nested_form[:allow_destroy] - = nested_form.link_to_remove ''.html_safe - = nested_form.generate({action: :nested, model_config: field.associated_model_config, nested_in: field }) diff --git a/app/views/rails_admin/main/_form_polymorphic_association.html.erb b/app/views/rails_admin/main/_form_polymorphic_association.html.erb new file mode 100644 index 0000000000..9805a7fdfb --- /dev/null +++ b/app/views/rails_admin/main/_form_polymorphic_association.html.erb @@ -0,0 +1,23 @@ +<% + column_type_dom_id = form.dom_id(field).sub(field.method_name.to_s, field.type_column) +%> + +
    +
    + <% field.widget_options_for_types.each do |model, value| %> +
    + <% end %> + <%= + form.select field.type_column, field.type_collection, {include_blank: true, selected: field.selected_type}, + class: "form-select", id: column_type_dom_id, data: { polymorphic: true, urls: field.type_urls.to_json }, + style: "float: left; margin-right: 10px;" + %> +
    +
    + <%= + form.select field.method_name, field.collection, {include_blank: true, selected: field.selected_id}, + class: "form-control", data: { filteringselect: true, options: field.widget_options }, + placeholder: 'Search' + %> +
    +
    diff --git a/app/views/rails_admin/main/_form_polymorphic_association.html.haml b/app/views/rails_admin/main/_form_polymorphic_association.html.haml deleted file mode 100644 index f10fcaa0ec..0000000000 --- a/app/views/rails_admin/main/_form_polymorphic_association.html.haml +++ /dev/null @@ -1,26 +0,0 @@ -:ruby - type_collection = field.polymorphic_type_collection - type_column = field.association.foreign_type.to_s - selected_type = field.bindings[:object].send(type_column) - selected = field.bindings[:object].send(field.association.name) - collection = selected ? [[field.formatted_value, selected.id]] : [[]] - column_type_dom_id = form.dom_id(field).sub(field.method_name.to_s, type_column) - current_action = params[:action].in?(['create', 'new']) ? 'create' : 'update' - - default_options = { float_left: false } - - js_data = type_collection.inject({}) do |options, model| - model_name = model.second.underscore.downcase - source_abstract_model = RailsAdmin.config(form.object.class).abstract_model - options.merge(model_name.gsub("_", "") => { - xhr: true, - remote_source: index_path(model_name, source_object_id: form.object.id, source_abstract_model: source_abstract_model.to_param, current_action: current_action, compact: true), - float_left: false - }) - end - -.form-inline - - js_data.each do |model, value| - %div{id: "#{model}-js-options", data: { options: value.to_json } } - = form.select type_column, type_collection, {include_blank: true, selected: selected_type}, class: "form-control", id: column_type_dom_id, data: { polymorphic: true, urls: field.polymorphic_type_urls.to_json }, style: "float: left; margin-right: 10px;" - = form.select field.method_name, collection, {include_blank: true, selected: selected.try(:id)}, class: "form-control", data: { filteringselect: true, options: js_data[selected_type.try(:downcase)] || default_options }, placeholder: 'Search' diff --git a/app/views/rails_admin/main/_form_simple_mde.haml b/app/views/rails_admin/main/_form_simple_mde.haml deleted file mode 100644 index 99cd82d98c..0000000000 --- a/app/views/rails_admin/main/_form_simple_mde.haml +++ /dev/null @@ -1,8 +0,0 @@ -:ruby - js_data = { - js_location: field.js_location, - css_location: field.css_location, - instance_config: field.instance_config - } - -= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: 'simplemde', options: js_data.to_json }).reverse_merge({ value: field.form_value }) diff --git a/app/views/rails_admin/main/_form_simple_mde.html.erb b/app/views/rails_admin/main/_form_simple_mde.html.erb new file mode 100644 index 0000000000..d86d5ee5ad --- /dev/null +++ b/app/views/rails_admin/main/_form_simple_mde.html.erb @@ -0,0 +1,8 @@ +<% + js_data = { + js_location: field.js_location, + css_location: field.css_location, + instance_config: field.instance_config + } +%> +<%= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: 'simplemde', options: js_data.to_json }).reverse_merge({ value: field.form_value }) %> diff --git a/app/views/rails_admin/main/_form_text.html.erb b/app/views/rails_admin/main/_form_text.html.erb new file mode 100644 index 0000000000..e478dc06ba --- /dev/null +++ b/app/views/rails_admin/main/_form_text.html.erb @@ -0,0 +1 @@ +<%= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: false, options: {}.to_json }).reverse_merge({ value: field.form_value, class: 'form-control', required: field.required }) %> diff --git a/app/views/rails_admin/main/_form_text.html.haml b/app/views/rails_admin/main/_form_text.html.haml deleted file mode 100644 index fafc9b9b50..0000000000 --- a/app/views/rails_admin/main/_form_text.html.haml +++ /dev/null @@ -1 +0,0 @@ -= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: false, options: {}.to_json }).reverse_merge({ value: field.form_value, class: 'form-control', required: field.required }) diff --git a/app/views/rails_admin/main/_form_wysihtml5.html.erb b/app/views/rails_admin/main/_form_wysihtml5.html.erb new file mode 100644 index 0000000000..d933be3181 --- /dev/null +++ b/app/views/rails_admin/main/_form_wysihtml5.html.erb @@ -0,0 +1,8 @@ +<% + js_data = { + csspath: field.css_location, + jspath: field.js_location, + config_options: field.config_options.to_json + } +%> +<%= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: 'bootstrap-wysihtml5', options: js_data.to_json }).reverse_merge({ value: field.form_value }) %> diff --git a/app/views/rails_admin/main/_form_wysihtml5.html.haml b/app/views/rails_admin/main/_form_wysihtml5.html.haml deleted file mode 100644 index ae362f0f66..0000000000 --- a/app/views/rails_admin/main/_form_wysihtml5.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -:ruby - js_data = { - csspath: field.css_location, - jspath: field.js_location, - config_options: field.config_options.to_json - } - -= form.text_area field.method_name, field.html_attributes.reverse_merge(data: { richtext: 'bootstrap-wysihtml5', options: js_data.to_json }).reverse_merge({ value: field.form_value }) diff --git a/app/views/rails_admin/main/_submit_buttons.html.erb b/app/views/rails_admin/main/_submit_buttons.html.erb new file mode 100644 index 0000000000..69f7fb36e4 --- /dev/null +++ b/app/views/rails_admin/main/_submit_buttons.html.erb @@ -0,0 +1,25 @@ +
    +
    + + + + <% if @action.enabled? && authorized?(:new, @abstract_model) %> + + <% end %> + <% if @action.enabled? && authorized?(:edit, @abstract_model) %> + + <% end %> + + +
    +
    diff --git a/app/views/rails_admin/main/_submit_buttons.html.haml b/app/views/rails_admin/main/_submit_buttons.html.haml deleted file mode 100644 index 885fbc1e7b..0000000000 --- a/app/views/rails_admin/main/_submit_buttons.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -.form-group.form-actions - .col-sm-offset-2.col-sm-10 - %input{type: :hidden, name: 'return_to', value: (params[:return_to].presence || request.referer)} - %button.btn.btn-primary{type: "submit", name: "_save", :'data-disable-with' => t("admin.form.save")} - %i.icon-white.icon-ok - = t("admin.form.save") - %span.extra_buttons - - if authorized? :new, @abstract_model - %button.btn.btn-info{type: "submit", name: "_add_another", :'data-disable-with' => t("admin.form.save_and_add_another")} - = t("admin.form.save_and_add_another") - - if authorized? :edit, @abstract_model - %button.btn.btn-info{type: "submit", name: "_add_edit", :'data-disable-with' => t("admin.form.save_and_edit")} - = t("admin.form.save_and_edit") - %button.btn.btn-default{type: "submit", name: "_continue", :'data-disable-with' => t("admin.form.cancel"), :formnovalidate => true} - %i.icon-remove - = t("admin.form.cancel") diff --git a/app/views/rails_admin/main/bulk_delete.html.erb b/app/views/rails_admin/main/bulk_delete.html.erb new file mode 100644 index 0000000000..82016b6173 --- /dev/null +++ b/app/views/rails_admin/main/bulk_delete.html.erb @@ -0,0 +1,19 @@ +

    + <%= I18n.t('admin.form.bulk_delete') %> +

    +
      + <%= render partial: "delete_notice", collection: @objects %> +
    +<%= form_tag bulk_delete_path(model_name: @abstract_model.to_param, bulk_ids: @objects.map(&:id)), method: :delete do %> +
    + + + +
    +<% end %> diff --git a/app/views/rails_admin/main/bulk_delete.html.haml b/app/views/rails_admin/main/bulk_delete.html.haml deleted file mode 100644 index fa50bb07cf..0000000000 --- a/app/views/rails_admin/main/bulk_delete.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -%h4= I18n.t('admin.form.bulk_delete') -%ul= render partial: "delete_notice", collection: @objects -= form_tag bulk_delete_path(model_name: @abstract_model.to_param, bulk_ids: @objects.map(&:id)), method: :delete do - .form-actions - %input{type: :hidden, name: 'return_to', value: (params[:return_to].presence || request.referer)} - %button.btn.btn-danger{type: "submit", :'data-disable-with' => t("admin.form.confirmation")} - %i.icon-white.icon-ok - = t("admin.form.confirmation") - %button.btn{type: "submit", name: "_continue", :'data-disable-with' => t("admin.form.cancel")} - %i.icon-remove - = t("admin.form.cancel") diff --git a/app/views/rails_admin/main/dashboard.html.erb b/app/views/rails_admin/main/dashboard.html.erb new file mode 100644 index 0000000000..ba9e39212f --- /dev/null +++ b/app/views/rails_admin/main/dashboard.html.erb @@ -0,0 +1,64 @@ +<% if @abstract_models %> + + + + + + + + + + + <% @abstract_models.each do |abstract_model| %> + <% if authorized? :index, abstract_model %> + <% index_path = index_path(model_name: abstract_model.to_param) %> + <% row_class = "#{cycle("odd", "even")}#{" link" if index_path} #{abstract_model.param_key}_links" %> + + <% last_created = @most_recent_created[abstract_model.model.name] %> + <% active = last_created.try(:today?) %> + + + + + + <% end %> + <% end %> + +
    + <%= t "admin.table_headers.model_name" %> + + <%= t "admin.table_headers.last_created" %> + + <%= t "admin.table_headers.records" %> +
    + + <%= link_to abstract_model.config.label_plural, index_path %> + + + <% if last_created %> + <%= t "admin.misc.time_ago", time: time_ago_in_words(last_created), default: "#{time_ago_in_words(last_created)} #{t("admin.misc.ago")}" %> + <% end %> + + <% count = @count[abstract_model.model.name] %> + <% percent = count > 0 ? (@max <= 1 ? count : ((Math.log(count+1) * 100.0) / Math.log(@max+1)).to_i) : -1 %> +
    +
    + <%= @count[abstract_model.model.name] %> +
    +
    +
    +<% end %> +<% if @history && authorized?(:history_index) %> +
    +
    +

    + <%= t("admin.actions.history_index.menu") %> +

    + <%= render partial: 'rails_admin/main/dashboard_history' %> +
    +
    +<% end %> diff --git a/app/views/rails_admin/main/dashboard.html.haml b/app/views/rails_admin/main/dashboard.html.haml deleted file mode 100644 index adcc2d0e95..0000000000 --- a/app/views/rails_admin/main/dashboard.html.haml +++ /dev/null @@ -1,34 +0,0 @@ -- if @abstract_models - %table.table.table-condensed.table-striped - %thead - %tr - %th.shrink.model-name= t "admin.table_headers.model_name" - %th.shrink.last-created= t "admin.table_headers.last_created" - %th.records= t "admin.table_headers.records" - %th.shrink.controls - %tbody - - @abstract_models.each do |abstract_model| - - if authorized? :index, abstract_model - - index_path = index_path(model_name: abstract_model.to_param) - - row_class = "#{cycle("odd", "even")}#{" link" if index_path} #{abstract_model.param_key}_links" - %tr{class: row_class, :"data-link" => index_path} - - last_created = @most_recent_created[abstract_model.model.name] - - active = last_created.try(:today?) - %td - %span.show= link_to capitalize_first_letter(abstract_model.config.label_plural), index_path, class: 'pjax' - %td - - if last_created - = t "admin.misc.time_ago", time: time_ago_in_words(last_created), default: "#{time_ago_in_words(last_created)} #{t("admin.misc.ago")}" - %td - - count = @count[abstract_model.model.name] - - percent = count > 0 ? (@max <= 1 ? count : ((Math.log(count+1) * 100.0) / Math.log(@max+1)).to_i) : -1 - .progress{style: "margin-bottom:0px", class: "progress-#{get_indicator(percent)} #{active && 'active progress-striped'}" } - .progress-bar.animate-width-to{:class => "progress-bar-#{get_indicator(percent)}", :'data-animate-length' => ([1.0, percent].max.to_i * 20), :'data-animate-width-to' => "#{[2.0, percent].max.to_i}%", style: "width:2%"} - = @count[abstract_model.model.name] - %td.links - %ul.inline.list-inline= menu_for :collection, abstract_model, nil, true -- if @auditing_adapter && authorized?(:history_index) - #block-tables.block - .content - %h2= t("admin.actions.history_index.menu") - = render partial: 'rails_admin/main/dashboard_history' diff --git a/app/views/rails_admin/main/delete.html.erb b/app/views/rails_admin/main/delete.html.erb new file mode 100644 index 0000000000..94cfcb6130 --- /dev/null +++ b/app/views/rails_admin/main/delete.html.erb @@ -0,0 +1,21 @@ +

    + <%= t("admin.form.are_you_sure_you_want_to_delete_the_object", model_name: @abstract_model.pretty_name.downcase) %> + <%= @model_config.with(object: @object).object_label %> + <%= t("admin.form.all_of_the_following_related_items_will_be_deleted") %> +

    +
      + <%= render partial: "delete_notice", object: @object %> +
    +<%= form_for(@object, url: delete_path(model_name: @abstract_model.to_param, id: @object.id), html: {method: "delete"}) do %> + +
    + + +
    +<% end %> diff --git a/app/views/rails_admin/main/delete.html.haml b/app/views/rails_admin/main/delete.html.haml deleted file mode 100644 index 5a437141ea..0000000000 --- a/app/views/rails_admin/main/delete.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -%h4 - = t("admin.form.are_you_sure_you_want_to_delete_the_object", model_name: @abstract_model.pretty_name.downcase) - “ - %strong>= @model_config.with(object: @object).object_label - \” - %span> - = t("admin.form.all_of_the_following_related_items_will_be_deleted") - -%ul= render partial: "delete_notice", object: @object -= form_for(@object, url: delete_path(model_name: @abstract_model.to_param, id: @object.id), html: {method: "delete"}) do - %input{type: :hidden, name: 'return_to', value: (params[:return_to].presence || request.referer)} - .form-actions - %button.btn.btn-danger{type: "submit", :'data-disable-with' => t("admin.form.confirmation")} - %i.icon-white.icon-ok - = t("admin.form.confirmation") - %button.btn{type: "submit", name: "_continue", :'data-disable-with' => t("admin.form.cancel")} - %i.icon-remove - = t("admin.form.cancel") diff --git a/app/views/rails_admin/main/edit.html.erb b/app/views/rails_admin/main/edit.html.erb new file mode 100644 index 0000000000..dfa32a9980 --- /dev/null +++ b/app/views/rails_admin/main/edit.html.erb @@ -0,0 +1,3 @@ +<%= rails_admin_form_for @object, url: edit_path(@abstract_model, @object.id), as: @abstract_model.param_key, html: { method: "put", multipart: true, class: "main", data: { title: @page_name } } do |form| %> + <%= form.generate action: :update %> +<% end %> diff --git a/app/views/rails_admin/main/edit.html.haml b/app/views/rails_admin/main/edit.html.haml deleted file mode 100644 index 33fe61581b..0000000000 --- a/app/views/rails_admin/main/edit.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -= rails_admin_form_for @object, url: edit_path(@abstract_model, @object.id), as: @abstract_model.param_key, html: { method: "put", multipart: true, class: "form-horizontal denser", data: { title: @page_name } } do |form| - = form.generate action: :update diff --git a/app/views/rails_admin/main/export.html.erb b/app/views/rails_admin/main/export.html.erb new file mode 100644 index 0000000000..7e84ab2d0e --- /dev/null +++ b/app/views/rails_admin/main/export.html.erb @@ -0,0 +1,149 @@ +<% params = request.params.except(:action, :controller, :utf8, :page, :per_page, :format, :authenticity_token) %> +<% visible_fields = @model_config.export.with(view: self, object: @abstract_model.model.new, controller: self.controller).visible_fields %> +<%= form_tag export_path(params.merge(all: true)), method: 'post', class: "main", data: {turbo: false} do %> + +
    + + + <%= t('admin.export.select') %> + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + <%= t('admin.export.fields_from', name: @model_config.label_plural.downcase) %> +
    +
    +
    +
    + <% visible_fields.select{ |f| !f.association? || f.association.polymorphic? }.each do |field| %> + <% list = field.virtual? ? 'methods' : 'only' %> +
    + <% if field.association? && field.association.polymorphic? %> + + <% polymorphic_type_column_name = @abstract_model.properties.detect {|p| field.association.foreign_type == p.name }.name %> + + <% else %> + + <% end %> +
    + <% end %> +
    +
    +
    +
    + <% visible_fields.select{ |f| f.association? && !f.association.polymorphic? }.each do |field| %> + <% fields = field.associated_model_config.export.with(controller: self.controller, view: self, object: (associated_model = field.associated_model_config.abstract_model.model).new).visible_fields.select{ |f| !f.association? } %> +
    +
    +
    +
    + <%= t('admin.export.fields_from_associated', name: field.label.downcase) %> +
    +
    +
    +
    + <% fields.each do |associated_model_field| %> + <% list = associated_model_field.virtual? ? 'methods' : 'only' %> +
    + +
    + <% end %> +
    +
    +
    +
    + <% end %> +
    +
    + + + <%= t('admin.export.options_for', name: 'csv') %> + +
    + <% guessed_encoding = @abstract_model.encoding %> + +
    +
    + <%= select_tag 'csv_options[encoding_to]', options_for_select(Encoding.name_list.sort), include_blank: true, placeholder: t('admin.misc.search'), :'data-enumeration' => true %> +
    +

    + <%= t('admin.export.csv.encoding_to_help', name: guessed_encoding) %> +

    +
    +
    +
    + +
    +
    + +
    +

    + <%= t('admin.export.csv.skip_header_help') %> +

    +
    +
    +
    + +
    +
    + <%= select_tag 'csv_options[generator][col_sep]', options_for_select({ '' => t('admin.export.csv.default_col_sep'), " ','" => ',', " ';'" => ';', '' => "'\t'" }), placeholder: t('admin.misc.search'), :'data-enumeration' => true %> +
    +

    + <%= t('admin.export.csv.col_sep_help', value: t('admin.export.csv.default_col_sep')) %> +

    +
    +
    +
    +
    +
    + + + + + +
    +
    +<% end %> diff --git a/app/views/rails_admin/main/export.html.haml b/app/views/rails_admin/main/export.html.haml deleted file mode 100644 index 0cc415d486..0000000000 --- a/app/views/rails_admin/main/export.html.haml +++ /dev/null @@ -1,92 +0,0 @@ -- params = request.params.except(:action, :controller, :utf8, :page, :per_page, :format, :authenticity_token) -- visible_fields = @model_config.export.with(view: self, object: @abstract_model.model.new, controller: self.controller).visible_fields - -= form_tag export_path(params.merge(all: true)), method: 'post', class: 'form-horizontal' do - - %input{name: "send_data", type: "hidden", value: "true"}/ - %fieldset{id: 'fields_to_export'} - %legend - %i.icon-chevron-down - = t('admin.export.select') - .form-group.control-group - .col-sm-12 - .checkbox - %label{for: 'check_all'} - = check_box_tag 'all', 'all', true, { id: 'check_all' } - %b= t('admin.export.select_all_fields') - .form-group.control-group - .col-sm-12 - .well.well-sm.reverse-selection{rel: 'tooltip', title: t('admin.export.click_to_reverse_selection'), style: 'margin: 0; cursor: pointer;'} - %b= t('admin.export.fields_from', name: @model_config.label_plural.downcase) - .controls - .row - - visible_fields.select{ |f| !f.association? || f.association.polymorphic? }.each do |field| - - list = field.virtual? ? 'methods' : 'only' - .checkbox.col-sm-3 - - if field.association? && field.association.polymorphic? - %label{for: "schema_#{list}_#{field.method_name}"} - = check_box_tag "schema[#{list}][]", field.method_name, true, { id: "schema_#{list}_#{field.method_name}" } - = field.label + " [id]" - - polymorphic_type_column_name = @abstract_model.properties.detect {|p| field.association.foreign_type == p.name }.name - %label{for: "schema_#{list}_#{polymorphic_type_column_name}"} - = check_box_tag "schema[#{list}][]", polymorphic_type_column_name, true, { id: "schema_#{list}_#{polymorphic_type_column_name}" } - = capitalize_first_letter(field.label) + " [type]" - - else - %label{for: "schema_#{list}_#{field.name}"} - = check_box_tag "schema[#{list}][]", field.name, true, { id: "schema_#{list}_#{field.name}" } - = capitalize_first_letter(field.label) - - - visible_fields.select{ |f| f.association? && !f.association.polymorphic? }.each do |field| - - fields = field.associated_model_config.export.with(controller: self.controller, view: self, object: (associated_model = field.associated_model_config.abstract_model.model).new).visible_fields.select{ |f| !f.association? } - .form-group.control-group - .col-sm-12 - .well.well-sm.reverse-selection{rel: 'tooltip', title: t('admin.export.click_to_reverse_selection'), style: 'margin: 0; cursor: pointer;'} - %b= t('admin.export.fields_from_associated', name: field.label.downcase) - .controls - .row - - fields.each do |associated_model_field| - - list = associated_model_field.virtual? ? 'methods' : 'only' - .checkbox.col-sm-3 - %label{for: "schema_include_#{field.name}_#{list}_#{associated_model_field.name}"} - = check_box_tag "schema[include][#{field.name}][#{list}][]", associated_model_field.name, true, { id: "schema_include_#{field.name}_#{list}_#{associated_model_field.name}" } - = capitalize_first_letter(associated_model_field.label) - - %fieldset - %legend - %i.icon-chevron-down - = t('admin.export.options_for', name: 'csv') - .form-group.control-group - - guessed_encoding = @abstract_model.encoding - %label.col-sm-2.control-label{for: "csv_options_encoding_to"}= t('admin.export.csv.encoding_to') - .col-sm-10.controls - -# from http://books.google.com/support/partner/bin/answer.py?answer=30990 : - = select_tag 'csv_options[encoding_to]', options_for_select(Encoding.name_list.sort), include_blank: true, placeholder: t('admin.misc.search'), :'data-enumeration' => true - %p.help-block= t('admin.export.csv.encoding_to_help', name: guessed_encoding) - - .form-group.control-group - %label.col-sm-2.control-label{for: "csv_options_skip_header"}= t('admin.export.csv.skip_header') - .col-sm-10.controls - .checkbox - %label{style: 'display: block;' } - = check_box_tag 'csv_options[skip_header]', 'true' - %p.help-block= t('admin.export.csv.skip_header_help') - - .form-group.control-group - %label.col-sm-2.control-label{for: "csv_options_generator_col_sep"}= t('admin.export.csv.col_sep') - .col-sm-10.controls - = select_tag 'csv_options[generator][col_sep]', options_for_select({ '' => t('admin.export.csv.default_col_sep'), " ','" => ',', " ';'" => ';', '' => "'\t'" }), placeholder: t('admin.misc.search'), :'data-enumeration' => true - %p.help-block= t('admin.export.csv.col_sep_help', value: t('admin.export.csv.default_col_sep')) - - .form-group.form-actions - .col-sm-offset-2.col-sm-10 - %input{type: :hidden, name: 'return_to', value: (params[:return_to].presence || request.referer)} - %button.btn.btn-primary{type: "submit", name: 'csv'} - %i.icon-white.icon-ok - = t("admin.export.confirmation", name: 'csv') - %button.btn.btn-info{type: "submit", name: 'json'} - = t("admin.export.confirmation", name: 'json') - %button.btn.btn-info{type: "submit", name: 'xml'} - = t("admin.export.confirmation", name: 'xml') - %button.btn{type: "submit", name: "_continue"} - %i.icon-remove - = t("admin.form.cancel") diff --git a/app/views/rails_admin/main/history.html.erb b/app/views/rails_admin/main/history.html.erb new file mode 100644 index 0000000000..4ea53ad50c --- /dev/null +++ b/app/views/rails_admin/main/history.html.erb @@ -0,0 +1,80 @@ +<% params = request.params.except(:action, :controller, :model_name) %> +<% query = params[:query] %> +<% filter = params[:filter] %> +<% sort = params[:sort] %> +<% sort_reverse = params[:sort_reverse] %> +<% path_method = params[:id] ? "history_show_path" : "history_index_path" %> +<%= form_tag("", method: "get", class: "search form-inline") do %> +
    +
    +
    +
    + " type="search" value="<%= query %>" /> + +
    +
    +
    +
    +<% end %> + + + + <% columns = [] %> + <% columns << { property_name: "created_at", css_class: "created_at",link_text: t('admin.table_headers.created_at') } %> + <% columns << { property_name: "username", css_class: "username", link_text: t('admin.table_headers.username') } %> + <% columns << { property_name: "item", css_class: "item", link_text: t('admin.table_headers.item') } if @general %> + <% columns << { property_name: "message", css_class: "message", link_text: t('admin.table_headers.message') } %> + <% columns.each do |column| %> + <% property_name = column[:property_name] %> + <% selected = (sort == property_name) %> + <% sort_direction = (sort_reverse ? "headerSortUp" : "headerSortDown" if selected) %> + <% sort_location = send(path_method, params.except("sort_reverse").merge(model_name: @abstract_model.to_param, sort: property_name).merge(selected && sort_reverse != "true" ? {sort_reverse: "true"} : {})) %> + + <% end %> + + + + <% @history.each_with_index do |object, index| %> + + <% unless object.created_at.nil? %> + + <% end %> + + <% if @general %> + <% if o = @abstract_model.get(object.item) %> + <% label = o.send(@abstract_model.config.object_label_method) %> + <% if show_action = action(:show, @abstract_model, o) %> + + <% else %> + + <% end %> + <% else %> + + <% end %> + <% end %> + + + <% end %> + +
    + <%= column[:link_text] %> +
    + <%= l(object.created_at, format: :long, default: l(object.created_at, format: :long)) %> + + <%= object.username %> + + <%= link_to(label, url_for(action: show_action.action_name, model_name: @abstract_model.to_param, id: o.id)) %> + + <%= label %> + + <%= "#{@abstract_model.config.label} ##{object.item}" %> + + <%= object.message.in?(['delete', 'new']) ? t("admin.actions.#{object.message}.done").capitalize : object.message %> +
    +<% unless params[:all] || !@history.respond_to?(:current_page) %> + <%= paginate(@history, theme: 'ra-twitter-bootstrap') %> + <%= link_to(t("admin.misc.show_all"), send(path_method, params.merge(all: true)), class: "show-all btn btn-light") unless (tc = @history.total_count) <= @history.size || tc > 100 %> +<% end %> diff --git a/app/views/rails_admin/main/history.html.haml b/app/views/rails_admin/main/history.html.haml deleted file mode 100644 index 73dc9668ef..0000000000 --- a/app/views/rails_admin/main/history.html.haml +++ /dev/null @@ -1,51 +0,0 @@ -- params = request.params.except(:action, :controller, :model_name) -- query = params[:query] -- filter = params[:filter] -- sort = params[:sort] -- sort_reverse = params[:sort_reverse] -- path_method = params[:id] ? "history_show_path" : "history_index_path" - -= form_tag("", method: "get", class: "search pjax-form form-inline") do - .well - .input-group - %input.form-control.input-small{name: "query", type: "search", value: query, placeholder: "#{t("admin.misc.filter")}", class: 'input-small'} - %span.input-group-btn - %button.btn.btn-primary{type: 'submit', :'data-disable-with' => ' '.html_safe + t('admin.misc.refresh')} - %i.icon-white.icon-refresh - = t("admin.misc.refresh") -%table#history.table.table-striped.table-condensed - %thead - %tr - - columns = [] - - columns << { property_name: "created_at", css_class: "created_at",link_text: t('admin.table_headers.created_at') } - - columns << { property_name: "username", css_class: "username", link_text: t('admin.table_headers.username') } - - columns << { property_name: "item", css_class: "item", link_text: t('admin.table_headers.item') } if @general - - columns << { property_name: "message", css_class: "message", link_text: t('admin.table_headers.message') } - - - columns.each do |column| - - property_name = column[:property_name] - - selected = (sort == property_name) - - sort_direction = (sort_reverse ? "headerSortUp" : "headerSortDown" if selected) - - sort_location = send(path_method, params.except("sort_reverse").merge(model_name: @abstract_model.to_param, sort: property_name).merge(selected && sort_reverse != "true" ? {sort_reverse: "true"} : {})) - %th{class: "header pjax #{column[:css_class]} #{sort_direction if selected}", :'data-href' => sort_location}= column[:link_text] - %tbody - - @history.each_with_index do |object, index| - %tr - - unless object.created_at.nil? - %td= l(object.created_at, format: :long, default: l(object.created_at, format: :long)) - %td= object.username - - if @general - - if o = @abstract_model.get(object.item) - - label = o.send(@abstract_model.config.object_label_method) - - if show_action = action(:show, @abstract_model, o) - %td= link_to(label, url_for(action: show_action.action_name, model_name: @abstract_model.to_param, id: o.id), class: 'pjax') - - else - %td= label - - else - %td= "#{@abstract_model.config.label} ##{object.item}" - %td= object.message.in?(['delete', 'new']) ? t("admin.actions.#{object.message}.done").capitalize : object.message - -- unless params[:all] || !@history.respond_to?(:current_page) - = paginate(@history, theme: 'ra-twitter-bootstrap', remote: true) - = link_to(t("admin.misc.show_all"), send(path_method, params.merge(all: true)), class: "show-all btn pjax") unless (tc = @history.total_count) <= @history.size || tc > 100 - diff --git a/app/views/rails_admin/main/index.html.erb b/app/views/rails_admin/main/index.html.erb new file mode 100644 index 0000000000..54c32cab2c --- /dev/null +++ b/app/views/rails_admin/main/index.html.erb @@ -0,0 +1,184 @@ +<% + query = params[:query] + params = request.params.except(:authenticity_token, :action, :controller, :utf8, :bulk_export) + params.delete(:query) if params[:query].blank? + params.delete(:sort_reverse) unless params[:sort_reverse] == 'true' + sort_reverse = params[:sort_reverse] + sort = params[:sort] + params.delete(:sort) if params[:sort] == @model_config.list.sort_by.to_s + export_action = RailsAdmin::Config::Actions.find(:export, { controller: self.controller, abstract_model: @abstract_model }) + export_action = nil unless export_action && authorized?(export_action.authorization_key, @abstract_model) + description = RailsAdmin.config(@abstract_model.model_name).description + properties = @model_config.list.with(controller: self.controller, view: self, object: @abstract_model.model.new).fields_for_table + checkboxes = @model_config.list.checkboxes? + table_table_header_count = begin + count = checkboxes ? 1 : 0 + count = count + properties.count + end +%> + +<% content_for :contextual_tabs do %> + <% if filterable_fields.present? %> + + <% end %> + <% if checkboxes %> + <%= bulk_menu %> + <% end %> +<% end %> + + + +
    + <%= form_tag(index_path(params.except(*%w[page f query])), method: :get) do %> +
    +
    +
    +
    +
    +
    + " type="search" value="<%= query %>" /> + + +
    + <% if @model_config.list.search_help.present? %> +
    <%= @model_config.list.search_help %>
    + <% end %> +
    +
    + <% if export_action %> + <%= link_to wording_for(:link, export_action), export_path(params.except('page')), class: 'btn btn-info' %> + <% end %> +
    +
    +
    + <% end %> + <% unless @model_config.list.scopes.empty? %> + + <% end %> + <%= form_tag bulk_action_path(model_name: @abstract_model.to_param), method: :post, id: "bulk_form", class: ["form", "mb-3"] do %> + <%= hidden_field_tag :bulk_action %> + <% if description.present? %> +

    + + <%= description %> + +

    + <% end %> +
    + + + + <% if checkboxes %> + + <% end %> + <% properties.each do |property| %> + <% selected = (sort == property.name.to_s) %> + <% if property.sortable %> + <% sort_location = index_path params.except('sort_reverse').except('page').merge(sort: property.name).merge(selected && sort_reverse != "true" ? {sort_reverse: "true"} : {}) %> + <% sort_direction = (sort_reverse == 'true' ? "headerSortUp" : "headerSortDown" if selected) %> + <% end %> + + <% end %> + + + + + <% @objects.each do |object| %> + + <% if checkboxes %> + + <% end %> + <% properties.map{ |property| property.bind(:object, object) }.each do |property| %> + <% value = property.pretty_value %> + <%= content_tag(:td, class: [property.sticky? && 'sticky', property.css_class, property.type_css_class].select(&:present?), title: strip_tags(value.to_s)) do %> + <%= value %> + <% end %> + <% end %> + + + <% end %> + <% if @objects.empty? %> + + + + <% end %> + +
    + + " data-href="<%= property.sortable && sort_location %>" rel="tooltip" title="<%= property.hint %>"> + <%= property.label %> +
    + <%= check_box_tag "bulk_ids[]", object.id.to_s, false %> +
    + <%= I18n.t('admin.actions.index.no_records') %> +
    +
    + <% if @model_config.list.limited_pagination %> +
    +
    + <%= paginate(@objects, theme: 'ra-twitter-bootstrap/without_count', total_pages: Float::INFINITY) %> +
    +
    + <% elsif @objects.respond_to?(:total_count) %> + <% total_count = @objects.total_count.to_i %> +
    +
    + <%= paginate(@objects, theme: 'ra-twitter-bootstrap') %> +
    +
    +
    +
    + <%= link_to(t("admin.misc.show_all"), index_path(params.merge(all: true)), class: "show-all btn btn-light clearfix") unless total_count > 100 || total_count <= @objects.to_a.size %> +
    +
    +
    + <%= "#{total_count} #{@model_config.pluralize(total_count).downcase}" %> +
    + <% else %> +
    + <%= "#{@objects.size} #{@model_config.pluralize(@objects.size).downcase}" %> +
    + <% end %> + <% end %> +
    diff --git a/app/views/rails_admin/main/index.html.haml b/app/views/rails_admin/main/index.html.haml deleted file mode 100644 index 21bd89d457..0000000000 --- a/app/views/rails_admin/main/index.html.haml +++ /dev/null @@ -1,126 +0,0 @@ -:ruby - query = params[:query] - params = request.params.except(:authenticity_token, :action, :controller, :utf8, :bulk_export, :_pjax) - params.delete(:query) if params[:query].blank? - params.delete(:sort_reverse) unless params[:sort_reverse] == 'true' - sort_reverse = params[:sort_reverse] - sort = params[:sort] - params.delete(:sort) if params[:sort] == @model_config.list.sort_by.to_s - export_action = RailsAdmin::Config::Actions.find(:export, { controller: self.controller, abstract_model: @abstract_model }) - export_action = nil unless export_action && authorized?(export_action.authorization_key, @abstract_model) - description = RailsAdmin.config(@abstract_model.model_name).description - properties = @model_config.list.with(controller: self.controller, view: self, object: @abstract_model.model.new).visible_fields - checkboxes = @model_config.list.checkboxes? - # columns paginate - unless (frozen_columns = @model_config.list.sidescroll_frozen_columns) - sets = get_column_sets(properties) - properties = sets[params[:set].to_i] || [] - other_left = ((params[:set].to_i - 1) >= 0) && sets[params[:set].to_i - 1].present? - other_right = sets[params[:set].to_i + 1].present? - end - -- content_for :contextual_tabs do - - if checkboxes - = bulk_menu - - if filterable_fields.present? - %li.dropdown{style: 'float:right'} - %a.dropdown-toggle{href: '#', :'data-toggle' => "dropdown"} - = t('admin.misc.add_filter') - %b.caret - %ul.dropdown-menu#filters{style: 'left:auto; right:0;'} - - filterable_fields.each do |field| - - field_options = case field.type - - when :enum - - options_for_select(field.with(object: @abstract_model.model.new).enum) - - else - - '' - %li - %a{href: '#', :"data-field-label" => field.label, :"data-field-name" => field.name, :"data-field-operator" => field.default_filter_operator, :"data-field-options" => field_options.html_safe, :"data-field-required" => field.required.to_s, :"data-field-type" => field.type, :"data-field-value" => "", :"data-field-datetimepicker-format" => (field.try(:parser) && field.parser.to_momentjs)}= capitalize_first_letter(field.label) - -%style - - properties.select{ |p| p.column_width.present? }.each do |property| - = "#list th.#{property.css_class} { width: #{property.column_width}px; min-width: #{property.column_width}px; }" - = "#list td.#{property.css_class} { max-width: #{property.column_width}px; }" - -#list - = form_tag(index_path(params.except(*%w[page f query])), method: :get, class: "pjax-form form-inline") do - .well - %span#filters_box{data: {options: ordered_filter_options.to_json}} - %hr.filters_box{style: "display:#{ordered_filters.empty? ? 'none' : 'block'}"} - .input-group - %input.form-control.input-small{name: "query", type: "search", value: query, placeholder: t("admin.misc.filter")} - %span.input-group-btn - %button.btn.btn-primary{type: 'submit', :'data-disable-with' => ' '.html_safe + t('admin.misc.refresh')} - %i.icon-white.icon-refresh - = t('admin.misc.refresh') - %button#remove_filter.btn.btn-info{title: t('admin.misc.reset_filters')} - %i.icon-white.icon-remove - - if export_action - %span{style: 'float:right'}= link_to wording_for(:link, export_action), export_path(params.except('set').except('page')), class: 'btn btn-info' - - - unless @model_config.list.scopes.empty? - %ul.nav.nav-tabs#scope_selector - - @model_config.list.scopes.each_with_index do |scope, index| - - scope = '_all' if scope.nil? - %li{class: "#{'active' if scope.to_s == params[:scope] || (params[:scope].blank? && index == 0)}"} - %a{href: index_path(params.merge(scope: scope, page: nil)), class: 'pjax'}= I18n.t("admin.scopes.#{@abstract_model.to_param}.#{scope}", default: I18n.t("admin.scopes.#{scope}", default: scope.to_s.titleize)) - - = form_tag bulk_action_path(model_name: @abstract_model.to_param), method: :post, id: "bulk_form", class: ["form", frozen_columns ? 'ra-sidescroll' : nil], data: (frozen_columns ? {ra_sidescroll: frozen_columns} : {}) do - = hidden_field_tag :bulk_action - - if description.present? - %p - %strong= description - - %table.table.table-condensed.table-striped - %thead - %tr - - if checkboxes - %th.shrink - %input.toggle{type: "checkbox"} - - if frozen_columns - %th.last.shrink - - if other_left - %th.other.left.shrink= "..." - - properties.each do |property| - - selected = (sort == property.name.to_s) - - if property.sortable - - sort_location = index_path params.except('sort_reverse').except('page').merge(sort: property.name).merge(selected && sort_reverse != "true" ? {sort_reverse: "true"} : {}) - - sort_direction = (sort_reverse == 'true' ? "headerSortUp" : "headerSortDown" if selected) - %th{class: "#{property.sortable && "header pjax" || nil} #{sort_direction if property.sortable && sort_direction} #{property.css_class} #{property.type_css_class}", :'data-href' => (property.sortable && sort_location), rel: "tooltip", title: "#{property.hint}"}= capitalize_first_letter(property.label) - - if other_right - %th.other.right.shrink= "..." - - unless frozen_columns - %th.last.shrink - %tbody - - @objects.each do |object| - %tr{class: "#{@abstract_model.param_key}_row #{@model_config.list.with(object: object).row_css_class}"} - - if checkboxes - %td= check_box_tag "bulk_ids[]", object.id, false - - if frozen_columns - %td.last.links - %ul.inline.list-inline= menu_for :member, @abstract_model, object, true - - if @other_left_link ||= other_left && index_path(params.except('set').merge(params[:set].to_i != 1 ? {set: (params[:set].to_i - 1)} : {})) - %td.other.left= link_to "...", @other_left_link, class: 'pjax' - - properties.map{ |property| property.bind(:object, object) }.each do |property| - - value = property.pretty_value - %td{class: "#{property.css_class} #{property.type_css_class}", title: strip_tags(value.to_s)}= value - - if @other_right_link ||= other_right && index_path(params.merge(set: (params[:set].to_i + 1))) - %td.other.right= link_to "...", @other_right_link, class: 'pjax' - - unless frozen_columns - %td.last.links - %ul.inline.list-inline= menu_for :member, @abstract_model, object, true - - - if @model_config.list.limited_pagination - .row - .col-md-6= paginate(@objects, theme: 'ra-twitter-bootstrap/without_count', total_pages: Float::INFINITY, remote: true) - - - elsif @objects.respond_to?(:total_count) - - total_count = @objects.total_count.to_i - .row - .col-md-6= paginate(@objects, theme: 'ra-twitter-bootstrap', remote: true) - .row - .col-md-6= link_to(t("admin.misc.show_all"), index_path(params.merge(all: true)), class: "show-all btn btn-default clearfix pjax") unless total_count > 100 || total_count <= @objects.to_a.size - .clearfix.total-count= "#{total_count} #{@model_config.pluralize(total_count).downcase}" - - - else - .clearfix.total-count= "#{@objects.size} #{@model_config.pluralize(@objects.size).downcase}" diff --git a/app/views/rails_admin/main/new.html.erb b/app/views/rails_admin/main/new.html.erb new file mode 100644 index 0000000000..e6ba02aaf4 --- /dev/null +++ b/app/views/rails_admin/main/new.html.erb @@ -0,0 +1,3 @@ +<%= rails_admin_form_for @object, url: new_path(model_name: @abstract_model.to_param), as: @abstract_model.param_key, html: { multipart: true, class: "main", data: { title: @page_name } } do |form| %> + <%= form.generate action: :create %> +<% end %> diff --git a/app/views/rails_admin/main/new.html.haml b/app/views/rails_admin/main/new.html.haml deleted file mode 100644 index dee547032d..0000000000 --- a/app/views/rails_admin/main/new.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -= rails_admin_form_for @object, url: new_path(model_name: @abstract_model.to_param), as: @abstract_model.param_key, html: { multipart: true, class: "form-horizontal denser", data: { title: @page_name } } do |form| - = form.generate action: :create diff --git a/app/views/rails_admin/main/show.html.erb b/app/views/rails_admin/main/show.html.erb new file mode 100644 index 0000000000..55ca7229b8 --- /dev/null +++ b/app/views/rails_admin/main/show.html.erb @@ -0,0 +1,30 @@ +<% @model_config.show.with(object: @object, view: self, controller: self.controller).visible_groups.each do |fieldset| %> + <% unless (fields = fieldset.with(object: @object, view: self, controller: self.controller).visible_fields).empty? %> + <% unless (fields = fields.reject{ |f| RailsAdmin::config.compact_show_view && (f.formatted_value.nil? || f.formatted_value == '') }).empty? %> +
    +

    + <%= fieldset.label %> +

    + <% if fieldset.help %> +

    + <%= fieldset.help %> +

    + <% end %> +
    + <% fields.each_with_index do |field, index| %> +
    +
    +
    + <%= field.label %> +
    +
    + <%= field.pretty_value %> +
    +
    +
    + <% end %> + +
    + <% end %> + <% end %> +<% end %> diff --git a/app/views/rails_admin/main/show.html.haml b/app/views/rails_admin/main/show.html.haml deleted file mode 100644 index f86de0e3fe..0000000000 --- a/app/views/rails_admin/main/show.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -- @model_config.show.with(object: @object, view: self, controller: self.controller).visible_groups.each do |fieldset| - - unless (fields = fieldset.with(object: @object, view: self, controller: self.controller).visible_fields).empty? - - unless (fields = fields.reject{ |f| RailsAdmin::config.compact_show_view && (f.formatted_value.nil? || f.formatted_value == '') }).empty? - .fieldset - %h4 - = fieldset.label - - if fieldset.help - %p= fieldset.help - %dl - - fields.each_with_index do |field, index| - %dt - %span.label.label-info{class: "#{field.type_css_class} #{field.css_class}"} - = field.label - %dd.well - = field.pretty_value diff --git a/config/initializers/active_record_extensions.rb b/config/initializers/active_record_extensions.rb index f2638805ee..b34f31a91a 100644 --- a/config/initializers/active_record_extensions.rb +++ b/config/initializers/active_record_extensions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ActiveSupport.on_load(:active_record) do module ActiveRecord class Base diff --git a/config/initializers/devise_patch.rb b/config/initializers/devise_patch.rb deleted file mode 100644 index f497508a4f..0000000000 --- a/config/initializers/devise_patch.rb +++ /dev/null @@ -1,9 +0,0 @@ -if defined?(::Devise) - module Devise - class FailureApp - def scope - @scope ||= warden_options[:scope] || Devise.default_scope - end - end - end -end diff --git a/config/initializers/haml.rb b/config/initializers/haml.rb deleted file mode 100644 index 26884716c9..0000000000 --- a/config/initializers/haml.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'haml' -require 'haml/template' -if Haml::Options.buffer_option_keys.include?(:ugly) - Haml::Template.options[:ugly] = true -end diff --git a/config/initializers/mongoid_extensions.rb b/config/initializers/mongoid_extensions.rb index 2c31419956..985e7ce711 100644 --- a/config/initializers/mongoid_extensions.rb +++ b/config/initializers/mongoid_extensions.rb @@ -1,4 +1,6 @@ +# frozen_string_literal: true + if defined?(::Mongoid::Document) require 'rails_admin/adapters/mongoid/extension' - Mongoid::Document.send(:include, RailsAdmin::Adapters::Mongoid::Extension) + Mongoid::Document.include RailsAdmin::Adapters::Mongoid::Extension end diff --git a/config/locales/rails_admin.en.yml b/config/locales/rails_admin.en.yml index d7d641b9ca..4a0730bd57 100644 --- a/config/locales/rails_admin.en.yml +++ b/config/locales/rails_admin.en.yml @@ -1,8 +1,8 @@ en: admin: js: - true: True - false: False + true: "True" + false: "False" is_present: Is present is_blank: Is blank date: Date ... @@ -11,13 +11,16 @@ en: yesterday: Yesterday this_week: This week last_week: Last week + time: Time ... number: Number ... contains: Contains + does_not_contain: Does not contain is_exactly: Is exactly starts_with: Starts with ends_with: Ends with too_many_objects: "Too many objects, use search box above" no_objects: "No objects found" + clear: Clear loading: "Loading..." toggle_navigation: Toggle navigation home: @@ -71,6 +74,7 @@ en: title: "List of %{model_label_plural}" menu: "List" breadcrumb: "%{model_label_plural}" + no_records: "No records found" show: title: "Details for %{model_label} '%{object_label}'" menu: "Show" @@ -126,6 +130,7 @@ en: save: "Save" save_and_add_another: "Save and add another" save_and_edit: "Save and edit" + delete_file: "Delete '%{field_label}' #%{number}" all_of_the_following_related_items_will_be_deleted: "? The following related items may be deleted or orphaned:" are_you_sure_you_want_to_delete_the_object: "Are you sure you want to delete this %{model_name}" confirmation: "Yes, I'm sure" @@ -140,7 +145,7 @@ en: display: "Display %{name}: %{type}" options_for: "Options for %{name}" empty_value_for_associated_objects: "" - click_to_reverse_selection: 'Click to reverse selection' + click_to_reverse_selection: "Click to reverse selection" csv: header_for_root_methods: "%{name}" # 'model' is available header_for_association_methods: "%{name} [%{association}]" diff --git a/config/routes.rb b/config/routes.rb index 00431a3126..a7686d036f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + RailsAdmin::Engine.routes.draw do controller 'main' do RailsAdmin::Config::Actions.all(:root).each { |action| match "/#{action.route_fragment}", action: action.action_name, as: action.action_name, via: action.http_methods } diff --git a/gemfiles/composite_primary_keys.gemfile b/gemfiles/composite_primary_keys.gemfile new file mode 100644 index 0000000000..1ebba5c55d --- /dev/null +++ b/gemfiles/composite_primary_keys.gemfile @@ -0,0 +1,55 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "appraisal", ">= 2.0" +gem "devise", "~> 4.7" +gem "net-smtp", require: false +gem "rails", "~> 7.0.0" +gem "sassc-rails", "~> 2.1" +gem "turbo-rails" +gem "vite_rails", require: false +gem "webpacker", require: false +gem "webrick" + +group :development, :test do + gem "pry", ">= 0.9" +end + +group :test do + gem "cancancan", "~> 3.0" + gem "carrierwave", [">= 2.0.0.rc", "< 3"] + gem "cuprite", "!= 0.15.1" + gem "database_cleaner-active_record", ">= 2.0", require: false + gem "dragonfly", "~> 1.0" + gem "factory_bot", ">= 4.2", "!= 6.4.5" + gem "generator_spec", ">= 0.8" + gem "kt-paperclip" + gem "launchy", ">= 2.2" + gem "mini_magick", ">= 3.4" + gem "pundit" + gem "rack-cache", require: "rack/cache" + gem "rspec-expectations", "!= 3.8.3" + gem "rspec-rails", ">= 4.0.0.beta2" + gem "rspec-retry" + gem "rubocop", ["~> 1.20", "!= 1.22.2"], require: false + gem "rubocop-performance", require: false + gem "shrine", "~> 3.0" + gem "simplecov", ">= 0.9", require: false + gem "simplecov-lcov", require: false + gem "timecop", ">= 0.5" + gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] +end + +group :active_record do + gem "paper_trail", ">= 12.0" + gem "composite_primary_keys" + + platforms :ruby, :mswin, :mingw, :x64_mingw do + gem "mysql2", ">= 0.3.14" + gem "pg", ">= 1.0.0" + gem "sqlite3", "~> 1.3" + end +end + +gemspec path: "../" diff --git a/gemfiles/rails_5.0.gemfile b/gemfiles/rails_5.0.gemfile deleted file mode 100644 index f4cce85278..0000000000 --- a/gemfiles/rails_5.0.gemfile +++ /dev/null @@ -1,69 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal", ">= 2.0" -gem "rails", "~> 5.0.0" -gem "haml" -gem "devise", "~> 4.0" -gem "sassc-rails", "~> 2.1" - -group :active_record do - gem "paper_trail", ">= 5.0" - - platforms :ruby, :mswin, :mingw, :x64_mingw do - gem "mysql2", ">= 0.3.14" - gem "sqlite3", "~> 1.3.0" - end - - platforms :jruby do - gem "activerecord-jdbcmysql-adapter", "~> 50.0" - gem "activerecord-jdbcpostgresql-adapter", "~> 50.0" - gem "activerecord-jdbcsqlite3-adapter", "~> 50.0" - end -end - -group :development, :test do - gem "pry", ">= 0.9" -end - -group :test do - gem "cancancan", "~> 2.0" - gem "carrierwave", [">= 2.0.0.rc", "< 3"] - gem "database_cleaner", [">= 1.2", "!= 1.4.0", "!= 1.5.0", "< 2.0"] - gem "dragonfly", "~> 1.0" - gem "factory_bot", ">= 4.2" - gem "generator_spec", ">= 0.8" - gem "launchy", ">= 2.2" - gem "mini_magick", ">= 3.4" - gem "poltergeist", "~> 1.5" - gem "pundit" - gem "rack-cache", require: "rack/cache" - gem "rspec-rails", ">= 2.14" - gem "rspec-expectations", "!= 3.8.3" - gem "rspec-retry" - gem "rubocop", "~> 0.68.1", require: false - gem "rubocop-performance", require: false - gem "simplecov", ">= 0.9", require: false - gem "simplecov-lcov", require: false - gem "timecop", ">= 0.5" - gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] - gem "paperclip", [">= 3.4", "!= 4.3.0"] - gem "shrine", "~> 2.13.0" - gem "shrine-memory" - - platforms :ruby_19 do - gem "tins", "~> 1.6.0", require: false - end -end - -group :mongoid do - gem "mongoid", "~> 6.1" - gem "kaminari-mongoid" - gem "mongoid-paperclip", ">= 0.0.8", require: "mongoid_paperclip" - gem "carrierwave-mongoid", ">= 0.6.3", require: "carrierwave/mongoid" - gem "cancancan-mongoid" - gem "shrine-mongoid", "~> 0.2.4" -end - -gemspec path: "../" diff --git a/gemfiles/rails_6.0.gemfile b/gemfiles/rails_6.0.gemfile index 19e0fbc54c..bcf2822642 100644 --- a/gemfiles/rails_6.0.gemfile +++ b/gemfiles/rails_6.0.gemfile @@ -3,66 +3,68 @@ source "https://rubygems.org" gem "appraisal", ">= 2.0" -gem "rails", "~> 6.0.0" -gem "haml" gem "devise", "~> 4.7" +gem "net-smtp", require: false +gem "rails", "~> 6.0.0" gem "sassc-rails", "~> 2.1" - -group :active_record do - gem "paper_trail", ">= 5.0" - gem "pg", ">= 1.0.0", platforms: :ruby - - platforms :ruby, :mswin, :mingw, :x64_mingw do - gem "mysql2", ">= 0.3.14" - gem "sqlite3", ">= 1.3" - end - - platforms :jruby do - gem "activerecord-jdbcmysql-adapter", "~> 60.0" - gem "activerecord-jdbcpostgresql-adapter", "~> 60.0" - gem "activerecord-jdbcsqlite3-adapter", "~> 60.0" - end -end +gem "turbo-rails", ["!= 2.0.8", "!= 2.0.9"] +gem "vite_rails", require: false +gem "webpacker", require: false +gem "webrick" +gem "psych", "~> 3.3" group :development, :test do gem "pry", ">= 0.9" end group :test do - gem "cancancan", "~> 3.0" + gem "cancancan", ["~> 3.0", "< 3.6"] gem "carrierwave", [">= 2.0.0.rc", "< 3"] - gem "database_cleaner", [">= 1.2", "!= 1.4.0", "!= 1.5.0", "< 2.0"] + gem "cuprite", "!= 0.15.1" + gem "database_cleaner-active_record", ">= 2.0", require: false gem "dragonfly", "~> 1.0" - gem "factory_bot", ">= 4.2" + gem "factory_bot", ">= 4.2", "!= 6.4.5" gem "generator_spec", ">= 0.8" + gem "kt-paperclip" gem "launchy", ">= 2.2" gem "mini_magick", ">= 3.4" - gem "poltergeist", "~> 1.5" - gem "pundit" + gem "pundit", "~> 2.1.0" gem "rack-cache", require: "rack/cache" - gem "rspec-rails", ">= 4.0.0.beta2" gem "rspec-expectations", "!= 3.8.3" + gem "rspec-rails", ">= 4.0.0.beta2" gem "rspec-retry" - gem "rubocop", "~> 0.68.1", require: false + gem "rubocop", ["~> 1.20", "!= 1.22.2"], require: false gem "rubocop-performance", require: false + gem "shrine", "~> 3.0" gem "simplecov", ">= 0.9", require: false gem "simplecov-lcov", require: false gem "timecop", ">= 0.5" gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] - gem "kt-paperclip" - gem "shrine", "~> 3.0" +end + +group :active_record do + gem "paper_trail", ">= 12.0" - platforms :ruby_19 do - gem "tins", "~> 1.6.0", require: false + platforms :ruby, :mswin, :mingw, :x64_mingw do + gem "mysql2", ">= 0.3.14" + gem "pg", ">= 1.0.0" + gem "sqlite3", "~> 1.3" + end + + platforms :jruby do + gem "activerecord-jdbcmysql-adapter", "~> 60.0" + gem "activerecord-jdbcpostgresql-adapter", "~> 60.0" + gem "activerecord-jdbcsqlite3-adapter", "~> 60.0" end end group :mongoid do - gem "mongoid", "~> 7.0" + gem "cancancan-mongoid" + gem "carrierwave-mongoid", ">= 0.6.3", require: "carrierwave/mongoid" + gem "database_cleaner-mongoid", ">= 2.0", require: false gem "kaminari-mongoid" + gem "mongoid", "~> 7.0" gem "mongoid-paperclip", ">= 0.0.8", require: "mongoid_paperclip" - gem "carrierwave-mongoid", ">= 0.6.3", require: "carrierwave/mongoid" - gem "cancancan-mongoid" gem "shrine-mongoid", "~> 1.0" end diff --git a/gemfiles/rails_6.1.gemfile b/gemfiles/rails_6.1.gemfile index c3a116ca2c..8c4b1c44c3 100644 --- a/gemfiles/rails_6.1.gemfile +++ b/gemfiles/rails_6.1.gemfile @@ -3,53 +3,68 @@ source "https://rubygems.org" gem "appraisal", ">= 2.0" -gem "rails", "~> 6.1.0" -gem "haml" gem "devise", "~> 4.7" +gem "net-smtp", require: false +gem "rails", "~> 6.1.0" gem "sassc-rails", "~> 2.1" -gem "webrick", "~> 1.7" - -group :active_record do - gem "paper_trail", ">= 5.0" - gem "pg", ">= 1.0.0", platforms: :ruby - - platforms :ruby, :mswin, :mingw, :x64_mingw do - gem "mysql2", ">= 0.3.14" - gem "sqlite3", ">= 1.3" - end -end +gem "turbo-rails" +gem "vite_rails", require: false +gem "webpacker", require: false +gem "webrick" group :development, :test do gem "pry", ">= 0.9" end group :test do - gem "cancancan", "~> 3.2" + gem "cancancan", "~> 3.0" gem "carrierwave", [">= 2.0.0.rc", "< 3"] - gem "database_cleaner", [">= 1.2", "!= 1.4.0", "!= 1.5.0", "< 2.0"] + gem "cuprite", "!= 0.15.1" + gem "database_cleaner-active_record", ">= 2.0", require: false gem "dragonfly", "~> 1.0" - gem "factory_bot", ">= 4.2" + gem "factory_bot", ">= 4.2", "!= 6.4.5" gem "generator_spec", ">= 0.8" + gem "kt-paperclip" gem "launchy", ">= 2.2" gem "mini_magick", ">= 3.4" - gem "poltergeist", "~> 1.5" gem "pundit" gem "rack-cache", require: "rack/cache" - gem "rspec-rails", ">= 4.0.0.beta2" gem "rspec-expectations", "!= 3.8.3" + gem "rspec-rails", ">= 4.0.0.beta2" gem "rspec-retry" - gem "rubocop", "~> 0.68.1", require: false + gem "rubocop", ["~> 1.20", "!= 1.22.2"], require: false gem "rubocop-performance", require: false + gem "shrine", "~> 3.0" gem "simplecov", ">= 0.9", require: false gem "simplecov-lcov", require: false gem "timecop", ">= 0.5" gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] - gem "kt-paperclip" - gem "shrine", "~> 3.0" +end + +group :active_record do + gem "paper_trail", ">= 12.0" - platforms :ruby_19 do - gem "tins", "~> 1.6.0", require: false + platforms :ruby, :mswin, :mingw, :x64_mingw do + gem "mysql2", ">= 0.3.14" + gem "pg", ">= 1.0.0" + gem "sqlite3", "~> 1.3" + end + + platforms :jruby do + gem "activerecord-jdbcmysql-adapter", "~> 61.0" + gem "activerecord-jdbcpostgresql-adapter", "~> 61.0" + gem "activerecord-jdbcsqlite3-adapter", "~> 61.0" end end +group :mongoid do + gem "cancancan-mongoid" + gem "carrierwave-mongoid", ">= 0.6.3", require: "carrierwave/mongoid" + gem "database_cleaner-mongoid", ">= 2.0", require: false + gem "kaminari-mongoid" + gem "mongoid", "~> 7.0" + gem "mongoid-paperclip", ">= 0.0.8", require: "mongoid_paperclip" + gem "shrine-mongoid", "~> 1.0" +end + gemspec path: "../" diff --git a/gemfiles/rails_5.2.gemfile b/gemfiles/rails_7.0.gemfile similarity index 57% rename from gemfiles/rails_5.2.gemfile rename to gemfiles/rails_7.0.gemfile index 77114acb11..36f7ff8cf6 100644 --- a/gemfiles/rails_5.2.gemfile +++ b/gemfiles/rails_7.0.gemfile @@ -3,66 +3,68 @@ source "https://rubygems.org" gem "appraisal", ">= 2.0" -gem "rails", "~> 5.2.0" -gem "haml" -gem "devise", "~> 4.4" +gem "devise", "~> 4.7" +gem "net-smtp", require: false +gem "rails", "~> 7.0.0" gem "sassc-rails", "~> 2.1" - -group :active_record do - gem "paper_trail", ">= 5.0" - gem "pg", ">= 1.0.0", platforms: :ruby - - platforms :ruby, :mswin, :mingw, :x64_mingw do - gem "mysql2", ">= 0.3.14" - gem "sqlite3", ">= 1.3" - end - - platforms :jruby do - gem "activerecord-jdbcmysql-adapter", "~> 52.0" - gem "activerecord-jdbcpostgresql-adapter", "~> 52.0" - gem "activerecord-jdbcsqlite3-adapter", "~> 52.0" - end -end +gem "turbo-rails" +gem "vite_rails", require: false +gem "webpacker", require: false +gem "webrick" +gem "importmap-rails", require: false group :development, :test do gem "pry", ">= 0.9" end group :test do - gem "cancancan", "~> 2.0" + gem "cancancan", "~> 3.0" gem "carrierwave", [">= 2.0.0.rc", "< 3"] - gem "database_cleaner", [">= 1.2", "!= 1.4.0", "!= 1.5.0", "< 2.0"] + gem "cuprite", "!= 0.15.1" + gem "database_cleaner-active_record", ">= 2.0", require: false gem "dragonfly", "~> 1.0" - gem "factory_bot", ">= 4.2" + gem "factory_bot", ">= 4.2", "!= 6.4.5" gem "generator_spec", ">= 0.8" + gem "kt-paperclip" gem "launchy", ">= 2.2" gem "mini_magick", ">= 3.4" - gem "poltergeist", "~> 1.5" gem "pundit" gem "rack-cache", require: "rack/cache" - gem "rspec-rails", ">= 2.14" gem "rspec-expectations", "!= 3.8.3" + gem "rspec-rails", ">= 4.0.0.beta2" gem "rspec-retry" - gem "rubocop", "~> 0.68.1", require: false + gem "rubocop", ["~> 1.20", "!= 1.22.2"], require: false gem "rubocop-performance", require: false + gem "shrine", "~> 3.0" gem "simplecov", ">= 0.9", require: false gem "simplecov-lcov", require: false gem "timecop", ">= 0.5" gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] - gem "paperclip", [">= 3.4", "!= 4.3.0"] - gem "shrine", "~> 3.0" +end + +group :active_record do + gem "paper_trail", ">= 12.0" + + platforms :ruby, :mswin, :mingw, :x64_mingw do + gem "mysql2", ">= 0.3.14" + gem "pg", ">= 1.0.0" + gem "sqlite3", "~> 1.3" + end - platforms :ruby_19 do - gem "tins", "~> 1.6.0", require: false + platforms :jruby do + gem "activerecord-jdbcmysql-adapter", "~> 70.0" + gem "activerecord-jdbcpostgresql-adapter", "~> 70.0" + gem "activerecord-jdbcsqlite3-adapter", "~> 70.0" end end group :mongoid do - gem "mongoid", "~> 7.0" + gem "cancancan-mongoid" + gem "carrierwave-mongoid", ">= 0.6.3", require: "carrierwave/mongoid" + gem "database_cleaner-mongoid", ">= 2.0", require: false gem "kaminari-mongoid" + gem "mongoid", "~> 8.0" gem "mongoid-paperclip", ">= 0.0.8", require: "mongoid_paperclip" - gem "carrierwave-mongoid", ">= 0.6.3", require: "carrierwave/mongoid" - gem "cancancan-mongoid" gem "shrine-mongoid", "~> 1.0" end diff --git a/gemfiles/rails_5.1.gemfile b/gemfiles/rails_7.1.gemfile similarity index 60% rename from gemfiles/rails_5.1.gemfile rename to gemfiles/rails_7.1.gemfile index 2ffc3b516a..98c7769fbc 100644 --- a/gemfiles/rails_5.1.gemfile +++ b/gemfiles/rails_7.1.gemfile @@ -3,66 +3,62 @@ source "https://rubygems.org" gem "appraisal", ">= 2.0" -gem "rails", "~> 5.1.0" -gem "haml" -gem "devise", "~> 4.0" +gem "devise", "~> 4.7" +gem "net-smtp", require: false +gem "rails", "~> 7.1.0" gem "sassc-rails", "~> 2.1" - -group :active_record do - gem "paper_trail", ">= 5.0" - gem "pg", "~> 0.14", platforms: :ruby - - platforms :ruby, :mswin, :mingw, :x64_mingw do - gem "mysql2", ">= 0.3.14" - gem "sqlite3", ">= 1.3" - end - - platforms :jruby do - gem "activerecord-jdbcmysql-adapter", "~> 51.0" - gem "activerecord-jdbcpostgresql-adapter", "~> 51.0" - gem "activerecord-jdbcsqlite3-adapter", "~> 51.0" - end -end +gem "turbo-rails" +gem "vite_rails", require: false +gem "webpacker", require: false +gem "webrick" +gem "importmap-rails", require: false group :development, :test do gem "pry", ">= 0.9" end group :test do - gem "cancancan", "~> 2.0" + gem "cancancan", "~> 3.0" gem "carrierwave", [">= 2.0.0.rc", "< 3"] - gem "database_cleaner", [">= 1.2", "!= 1.4.0", "!= 1.5.0", "< 2.0"] + gem "cuprite", "!= 0.15.1" + gem "database_cleaner-active_record", ">= 2.0", require: false gem "dragonfly", "~> 1.0" - gem "factory_bot", ">= 4.2" + gem "factory_bot", ">= 4.2", "!= 6.4.5" gem "generator_spec", ">= 0.8" + gem "kt-paperclip" gem "launchy", ">= 2.2" gem "mini_magick", ">= 3.4" - gem "poltergeist", "~> 1.5" gem "pundit" gem "rack-cache", require: "rack/cache" - gem "rspec-rails", ">= 2.14" gem "rspec-expectations", "!= 3.8.3" + gem "rspec-rails", ">= 4.0.0.beta2" gem "rspec-retry" - gem "rubocop", "~> 0.68.1", require: false + gem "rubocop", ["~> 1.20", "!= 1.22.2"], require: false gem "rubocop-performance", require: false + gem "shrine", "~> 3.0" gem "simplecov", ">= 0.9", require: false gem "simplecov-lcov", require: false gem "timecop", ">= 0.5" gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] - gem "paperclip", [">= 3.4", "!= 4.3.0"] - gem "shrine", "~> 3.0" +end - platforms :ruby_19 do - gem "tins", "~> 1.6.0", require: false +group :active_record do + gem "paper_trail", ">= 12.0" + + platforms :ruby, :mswin, :mingw, :x64_mingw do + gem "mysql2", ">= 0.3.14" + gem "pg", ">= 1.0.0" + gem "sqlite3", "~> 1.3" end end group :mongoid do - gem "mongoid", "~> 7.0" + gem "cancancan-mongoid" + gem "carrierwave-mongoid", ">= 0.6.3", require: "carrierwave/mongoid" + gem "database_cleaner-mongoid", ">= 2.0", require: false gem "kaminari-mongoid" + gem "mongoid", "~> 8.0" gem "mongoid-paperclip", ">= 0.0.8", require: "mongoid_paperclip" - gem "carrierwave-mongoid", ">= 0.6.3", require: "carrierwave/mongoid" - gem "cancancan-mongoid" gem "shrine-mongoid", "~> 1.0" end diff --git a/gemfiles/rails_7.2.gemfile b/gemfiles/rails_7.2.gemfile new file mode 100644 index 0000000000..f0edccff88 --- /dev/null +++ b/gemfiles/rails_7.2.gemfile @@ -0,0 +1,55 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "appraisal", ">= 2.0" +gem "devise", "~> 4.7" +gem "net-smtp", require: false +gem "rails", "~> 7.2.0" +gem "sassc-rails", "~> 2.1" +gem "turbo-rails" +gem "vite_rails", require: false +gem "webpacker", require: false +gem "webrick" +gem "importmap-rails", require: false + +group :development, :test do + gem "pry", ">= 0.9" +end + +group :test do + gem "cancancan", "~> 3.0" + gem "carrierwave", [">= 2.0.0.rc", "< 3"] + gem "cuprite", "!= 0.15.1" + gem "database_cleaner-active_record", ">= 2.0", require: false + gem "dragonfly", "~> 1.0" + gem "factory_bot", ">= 4.2", "!= 6.4.5" + gem "generator_spec", ">= 0.8" + gem "kt-paperclip" + gem "launchy", ">= 2.2" + gem "mini_magick", ">= 3.4" + gem "pundit" + gem "rack-cache", require: "rack/cache" + gem "rspec-expectations", "!= 3.8.3" + gem "rspec-rails", ">= 4.0.0.beta2" + gem "rspec-retry" + gem "rubocop", ["~> 1.20", "!= 1.22.2"], require: false + gem "rubocop-performance", require: false + gem "shrine", "~> 3.0" + gem "simplecov", ">= 0.9", require: false + gem "simplecov-lcov", require: false + gem "timecop", ">= 0.5" + gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] +end + +group :active_record do + gem "paper_trail", ">= 12.0" + + platforms :ruby, :mswin, :mingw, :x64_mingw do + gem "mysql2", ">= 0.3.14" + gem "pg", ">= 1.0.0" + gem "sqlite3", "~> 1.3" + end +end + +gemspec path: "../" diff --git a/lib/generators/rails_admin/importmap_formatter.rb b/lib/generators/rails_admin/importmap_formatter.rb new file mode 100644 index 0000000000..138a0e5a39 --- /dev/null +++ b/lib/generators/rails_admin/importmap_formatter.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'importmap/packager' + +module RailsAdmin + class ImportmapFormatter + attr_reader :packager + + def initialize(path = 'config/importmap.rails_admin.rb') + @packager = Importmap::Packager.new(path) + end + + def format + imports = packager.import("rails_admin@#{RailsAdmin::Version.js}", from: 'jspm.io') + + # Use ESM compatible version to work around https://github.com/cljsjs/packages/issues/1579 + imports['@popperjs/core'].gsub!('lib/index.js', 'dist/esm/popper.js') + + # Tidy up jQuery UI dependencies + jquery_uis = imports.keys.filter { |key, _| key =~ /jquery-ui/ } + imports['jquery-ui/'] = imports[jquery_uis.first].gsub(%r{(@[^/@]+)/[^@]+$}, '\1/') + imports.reject! { |key, _| jquery_uis.include? key } + + pins = ['pin "rails_admin", preload: true', packager.pin_for('rails_admin/src/rails_admin/base', imports.delete('rails_admin'))] + (pins + imports.map { |package, url| packager.pin_for(package, url) }).join("\n") + end + end +end diff --git a/lib/generators/rails_admin/install_generator.rb b/lib/generators/rails_admin/install_generator.rb index 208a6ec968..ca86af7bfc 100644 --- a/lib/generators/rails_admin/install_generator.rb +++ b/lib/generators/rails_admin/install_generator.rb @@ -1,18 +1,142 @@ +# frozen_string_literal: true + require 'rails/generators' -require File.expand_path('../utils', __FILE__) +require 'rails_admin/version' +require File.expand_path('utils', __dir__) module RailsAdmin class InstallGenerator < Rails::Generators::Base - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) include Generators::Utils::InstanceMethods argument :_namespace, type: :string, required: false, desc: 'RailsAdmin url namespace' + class_option :asset, type: :string, required: false, default: nil, desc: 'Asset delivery method [options: webpacker, webpack, sprockets, importmap, vite]' desc 'RailsAdmin installation generator' def install - namespace = ask_for('Where do you want to mount rails_admin?', 'admin', _namespace) - route("mount RailsAdmin::Engine => '/#{namespace}', as: 'rails_admin'") - template 'initializer.erb', 'config/initializers/rails_admin.rb' + if File.read(File.join(destination_root, 'config/routes.rb')).include?('mount RailsAdmin::Engine') + display "Skipped route addition, since it's already there" + else + namespace = ask_for('Where do you want to mount rails_admin?', 'admin', _namespace) + route("mount RailsAdmin::Engine => '/#{namespace}', as: 'rails_admin'") + end + if File.exist? File.join(destination_root, 'config/initializers/rails_admin.rb') + insert_into_file 'config/initializers/rails_admin.rb', " config.asset_source = :#{asset}\n", after: "RailsAdmin.config do |config|\n" + else + template 'initializer.erb', 'config/initializers/rails_admin.rb' + end + display "Using [#{asset}] for asset delivery method" + case asset + when 'webpack' + configure_for_webpack + when 'importmap' + configure_for_importmap + when 'webpacker' + configure_for_webpacker5 + when 'vite' + configure_for_vite + when 'sprockets' + configure_for_sprockets + else + raise "Unknown asset source: #{asset}" + end + end + + private + + def asset + return options['asset'] if options['asset'] + + if defined?(Webpacker) + 'webpacker' + elsif Rails.root.join('webpack.config.js').exist? + 'webpack' + elsif Rails.root.join('config/importmap.rb').exist? + 'importmap' + elsif defined?(ViteRuby) + 'vite' + else + 'sprockets' + end + end + + def configure_for_sprockets + gem 'sassc-rails' + end + + def configure_for_webpacker5 + run "yarn add rails_admin@#{RailsAdmin::Version.js}" + template 'rails_admin.webpacker.js', 'app/javascript/packs/rails_admin.js' + template 'rails_admin.scss.erb', 'app/javascript/stylesheets/rails_admin.scss' + # To work around https://github.com/railsadminteam/rails_admin/issues/3565 + add_package_json_field('resolutions', {'rails_admin/@fortawesome/fontawesome-free' => '^5.15.0'}) + end + + def configure_for_vite + vite_source_code_dir = ViteRuby.config.source_code_dir + run "yarn add rails_admin@#{RailsAdmin::Version.js} sass" + template('rails_admin.vite.js', File.join(vite_source_code_dir, 'entrypoints', 'rails_admin.js')) + @fa_font_path = '@fortawesome/fontawesome-free/webfonts' + template('rails_admin.scss.erb', File.join(vite_source_code_dir, 'stylesheets', 'rails_admin.scss')) + end + + def configure_for_webpack + run "yarn add rails_admin@#{RailsAdmin::Version.js}" + template 'rails_admin.js', 'app/javascript/rails_admin.js' + webpack_config = File.join(destination_root, 'webpack.config.js') + marker = %r{application: ["']./app/javascript/application.js["']} + if File.exist?(webpack_config) && File.read(webpack_config) =~ marker + insert_into_file 'webpack.config.js', %(,\n rails_admin: "./app/javascript/rails_admin.js"), after: marker + else + say 'Add `rails_admin: "./app/javascript/rails_admin.js"` to the entry section in your webpack.config.js.', :red + end + setup_css({'build' => 'webpack --config webpack.config.js'}) + end + + def configure_for_importmap + run "yarn add rails_admin@#{RailsAdmin::Version.js}" + template 'rails_admin.js', 'app/javascript/rails_admin.js' + require_relative 'importmap_formatter' + add_file 'config/importmap.rails_admin.rb', ImportmapFormatter.new.format + setup_css + end + + def setup_css(additional_script_entries = {}) + gem 'cssbundling-rails' + rake 'css:install:sass' + + @fa_font_path = '.' + template 'rails_admin.scss.erb', 'app/assets/stylesheets/rails_admin.scss' + asset_config = %{Rails.application.config.assets.paths << Rails.root.join("node_modules/@fortawesome/fontawesome-free/webfonts")\n} + if File.exist? File.join(destination_root, 'config/initializers/assets.rb') + append_to_file 'config/initializers/assets.rb', asset_config + else + add_file 'config/initializers/assets.rb', asset_config + end + add_package_json_field('scripts', additional_script_entries.merge({'build:css' => 'sass ./app/assets/stylesheets/rails_admin.scss:./app/assets/builds/rails_admin.css --no-source-map --load-path=node_modules'}), <<~INSTRUCTION) + Taking 'build:css' as an example, if you're already have application.sass.css for the sass build, the resulting script would look like: + sass ./app/assets/stylesheets/application.sass.scss:./app/assets/builds/application.css ./app/assets/stylesheets/rails_admin.scss:./app/assets/builds/rails_admin.css --no-source-map --load-path=node_modules + INSTRUCTION + end + + def add_package_json_field(name, entries, instruction = nil) + display "Add #{name} to package.json" + package = begin + JSON.parse(File.read(File.join(destination_root, 'package.json'))) + rescue Errno::ENOENT, JSON::ParserError + {} + end + if package[name] && (package[name].keys & entries.keys).any? + say <<~MESSAGE, :red + You need to merge "#{name}": #{JSON.pretty_generate(entries)} into the existing #{name} in your package.json.#{instruction && "\n#{instruction}"} + MESSAGE + else + package[name] ||= {} + entries.each do |entry, value| + package[name][entry] = value + end + add_file 'package.json', "#{JSON.pretty_generate(package)}\n" + end end end end diff --git a/lib/generators/rails_admin/templates/initializer.erb b/lib/generators/rails_admin/templates/initializer.erb index 0419a08891..0bc5eb8b46 100644 --- a/lib/generators/rails_admin/templates/initializer.erb +++ b/lib/generators/rails_admin/templates/initializer.erb @@ -1,4 +1,5 @@ RailsAdmin.config do |config| + config.asset_source = :<%= asset %> ### Popular gems integration @@ -17,7 +18,7 @@ RailsAdmin.config do |config| ## == PaperTrail == # config.audit_with :paper_trail, 'User', 'PaperTrail::Version' # PaperTrail >= 3.0.0 - ### More at https://github.com/sferik/rails_admin/wiki/Base-configuration + ### More at https://github.com/railsadminteam/rails_admin/wiki/Base-configuration ## == Gravatar integration == ## To disable Gravatar integration in Navigation Bar set to false diff --git a/lib/generators/rails_admin/templates/rails_admin.js b/lib/generators/rails_admin/templates/rails_admin.js new file mode 100644 index 0000000000..d0c3beda57 --- /dev/null +++ b/lib/generators/rails_admin/templates/rails_admin.js @@ -0,0 +1 @@ +import "rails_admin/src/rails_admin/base"; diff --git a/lib/generators/rails_admin/templates/rails_admin.scss.erb b/lib/generators/rails_admin/templates/rails_admin.scss.erb new file mode 100644 index 0000000000..93ba8b9bb6 --- /dev/null +++ b/lib/generators/rails_admin/templates/rails_admin.scss.erb @@ -0,0 +1 @@ +<%= @fa_font_path ? %{$fa-font-path: "#{@fa_font_path}";\n} : '' %>@import "rails_admin/src/rails_admin/styles/base"; diff --git a/lib/generators/rails_admin/templates/rails_admin.vite.js b/lib/generators/rails_admin/templates/rails_admin.vite.js new file mode 100644 index 0000000000..1248edc4ba --- /dev/null +++ b/lib/generators/rails_admin/templates/rails_admin.vite.js @@ -0,0 +1,2 @@ +import "~/stylesheets/rails_admin.scss"; +import "rails_admin/src/rails_admin/base"; diff --git a/lib/generators/rails_admin/templates/rails_admin.webpacker.js b/lib/generators/rails_admin/templates/rails_admin.webpacker.js new file mode 100644 index 0000000000..63c31b5973 --- /dev/null +++ b/lib/generators/rails_admin/templates/rails_admin.webpacker.js @@ -0,0 +1,2 @@ +import "rails_admin/src/rails_admin/base"; +import "../stylesheets/rails_admin.scss"; diff --git a/lib/generators/rails_admin/utils.rb b/lib/generators/rails_admin/utils.rb index 80f70d4e75..ffd70cefee 100644 --- a/lib/generators/rails_admin/utils.rb +++ b/lib/generators/rails_admin/utils.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Generators module Utils diff --git a/lib/rails_admin.rb b/lib/rails_admin.rb index 4a2dde9433..8c7887f64c 100644 --- a/lib/rails_admin.rb +++ b/lib/rails_admin.rb @@ -1,16 +1,20 @@ +# frozen_string_literal: true + require 'rails_admin/engine' require 'rails_admin/abstract_model' require 'rails_admin/config' +require 'rails_admin/config/const_load_suppressor' require 'rails_admin/extension' require 'rails_admin/extensions/cancancan' require 'rails_admin/extensions/pundit' require 'rails_admin/extensions/paper_trail' -require 'rails_admin/extensions/history' require 'rails_admin/support/csv_converter' require 'rails_admin/support/hash_helper' require 'yaml' module RailsAdmin + extend RailsAdmin::Config::ConstLoadSuppressor + # Setup RailsAdmin # # Given the first argument is a model class, a model class name @@ -18,7 +22,7 @@ module RailsAdmin # # If only a block is passed it is stored to initializer stack to be evaluated # on first request in production mode and on each request in development. If - # initialization has already occured (in other words RailsAdmin.setup has + # initialization has already occurred (in other words RailsAdmin.setup has # been called) the block will be added to stack and evaluated at once. # # Otherwise returns RailsAdmin::Config class. @@ -28,7 +32,7 @@ def self.config(entity = nil, &block) if entity RailsAdmin::Config.model(entity, &block) elsif block_given? - block.call(RailsAdmin::Config) + RailsAdmin::Config::ConstLoadSuppressor.suppressing { yield(RailsAdmin::Config) } else RailsAdmin::Config end @@ -63,5 +67,3 @@ def self.yaml_dump(object) YAML.dump(object) end end - -require 'rails_admin/bootstrap-sass' unless defined? Bootstrap diff --git a/lib/rails_admin/abstract_model.rb b/lib/rails_admin/abstract_model.rb index 3929995b1a..20884be487 100644 --- a/lib/rails_admin/abstract_model.rb +++ b/lib/rails_admin/abstract_model.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/support/datetime' module RailsAdmin @@ -19,7 +21,7 @@ def all(adapter = nil) def new(m) m = m.constantize unless m.is_a?(Class) (am = old_new(m)).model && am.adapter ? am : nil - rescue LoadError, NameError + rescue *([LoadError, NameError] + (defined?(ActiveRecord) ? ['ActiveRecord::NoDatabaseError'.constantize, 'ActiveRecord::ConnectionNotEstablished'.constantize] : [])) puts "[RailsAdmin] Could not load model #{m}, assuming model is non existing. (#{$ERROR_INFO})" unless Rails.env.test? nil end @@ -58,6 +60,14 @@ def model @model_name.constantize end + def quoted_table_name + table_name + end + + def quote_column_name(name) + name + end + def to_s model.to_s end @@ -86,9 +96,8 @@ def each_associated_children(object) associations.each do |association| case association.type when :has_one - if child = object.send(association.name) - yield(association, [child]) - end + child = object.send(association.name) + yield(association, [child]) if child when :has_many children = object.send(association.name) yield(association, Array.new(children)) @@ -96,6 +105,14 @@ def each_associated_children(object) end end + def format_id(id) + id + end + + def parse_id(id) + id + end + private def initialize_active_record @@ -124,6 +141,7 @@ def initialize(column, type, value, operator) def to_statement return if [@operator, @value].any? { |v| v == '_discard' } + unary_operators[@operator] || unary_operators[@value] || build_statement_for_type_generic end @@ -139,21 +157,22 @@ def build_statement_for_type_generic case @type when :date build_statement_for_date - when :datetime, :timestamp + when :datetime, :timestamp, :time build_statement_for_datetime_or_timestamp end end end def build_statement_for_type - raise('You must override build_statement_for_type in your StatementBuilder') + raise 'You must override build_statement_for_type in your StatementBuilder' end def build_statement_for_integer_decimal_or_float case @value - when Array then + when Array val, range_begin, range_end = *@value.collect do |v| next unless v.to_i.to_s == v || v.to_f.to_s == v + @type == :integer ? v.to_i : v.to_f end case @operator @@ -171,24 +190,36 @@ def build_statement_for_integer_decimal_or_float def build_statement_for_date start_date, end_date = get_filtering_duration - start_date = (start_date.to_date rescue nil) if start_date - end_date = (end_date.to_date rescue nil) if end_date + if start_date + start_date = begin + start_date.to_date + rescue StandardError + nil + end + end + if end_date + end_date = begin + end_date.to_date + rescue StandardError + nil + end + end range_filter(start_date, end_date) end def build_statement_for_datetime_or_timestamp start_date, end_date = get_filtering_duration - start_date = start_date.try(:beginning_of_day) if start_date - end_date = end_date.try(:end_of_day) if end_date + start_date = start_date.beginning_of_day if start_date.is_a?(Date) + end_date = end_date.end_of_day if end_date.is_a?(Date) range_filter(start_date, end_date) end def unary_operators - raise('You must override unary_operators in your StatementBuilder') + raise 'You must override unary_operators in your StatementBuilder' end def range_filter(_min, _max) - raise('You must override range_filter in your StatementBuilder') + raise 'You must override range_filter in your StatementBuilder' end class FilteringDuration diff --git a/lib/rails_admin/adapters/active_record.rb b/lib/rails_admin/adapters/active_record.rb index 2defcb1ab9..e179280e78 100644 --- a/lib/rails_admin/adapters/active_record.rb +++ b/lib/rails_admin/adapters/active_record.rb @@ -1,20 +1,24 @@ +# frozen_string_literal: true + require 'active_record' -require 'rails_admin/adapters/active_record/abstract_object' require 'rails_admin/adapters/active_record/association' +require 'rails_admin/adapters/active_record/object_extension' require 'rails_admin/adapters/active_record/property' module RailsAdmin module Adapters module ActiveRecord - DISABLED_COLUMN_TYPES = [:tsvector, :blob, :binary, :spatial, :hstore, :geometry].freeze + DISABLED_COLUMN_TYPES = %i[tsvector blob binary spatial hstore geometry].freeze def new(params = {}) - AbstractObject.new(model.new(params)) + model.new(params).extend(ObjectExtension) end - def get(id) - return unless object = model.where(primary_key => id).first - AbstractObject.new object + def get(id, scope = scoped) + object = primary_key_scope(scope, id).first + return unless object + + object.extend(ObjectExtension) end def scoped @@ -29,13 +33,11 @@ def all(options = {}, scope = nil) scope ||= scoped scope = scope.includes(options[:include]) if options[:include] scope = scope.limit(options[:limit]) if options[:limit] - scope = scope.where(primary_key => options[:bulk_ids]) if options[:bulk_ids] + scope = bulk_scope(scope, options) if options[:bulk_ids] scope = query_scope(scope, options[:query]) if options[:query] scope = filter_scope(scope, options[:filters]) if options[:filters] - if options[:page] && options[:per] - scope = scope.send(Kaminari.config.page_method_name, options[:page]).per(options[:per]) - end - scope = scope.reorder("#{options[:sort]} #{options[:sort_reverse] ? 'asc' : 'desc'}") if options[:sort] + scope = scope.send(Kaminari.config.page_method_name, options[:page]).per(options[:per]) if options[:page] && options[:per] + scope = sort_scope(scope, options) if options[:sort] scope end @@ -70,6 +72,14 @@ def base_class delegate :primary_key, :table_name, to: :model, prefix: false + def quoted_table_name + model.quoted_table_name + end + + def quote_column_name(name) + model.connection.quote_column_name(name) + end + def encoding adapter = if ::ActiveRecord::Base.respond_to?(:connection_db_config) @@ -81,9 +91,13 @@ def encoding when 'postgresql' ::ActiveRecord::Base.connection.select_one("SELECT ''::text AS str;").values.first.encoding when 'mysql2' - ::ActiveRecord::Base.connection.raw_connection.encoding + if RUBY_ENGINE == 'jruby' + ::ActiveRecord::Base.connection.select_one("SELECT '' AS str;").values.first.encoding + else + ::ActiveRecord::Base.connection.raw_connection.encoding + end when 'oracle_enhanced' - ::ActiveRecord::Base.connection.select_one("SELECT dummy FROM DUAL").values.first.encoding + ::ActiveRecord::Base.connection.select_one('SELECT dummy FROM DUAL').values.first.encoding else ::ActiveRecord::Base.connection.select_one("SELECT '' AS str;").values.first.encoding end @@ -101,6 +115,59 @@ def adapter_supports_joins? true end + def format_id(id) + if primary_key.is_a? Array + RailsAdmin.config.composite_keys_serializer.serialize(id) + else + id + end + end + + def parse_id(id) + if primary_key.is_a?(Array) + ids = RailsAdmin.config.composite_keys_serializer.deserialize(id) + primary_key.each_with_index do |key, i| + ids[i] = model.type_for_attribute(key).cast(ids[i]) + end + ids + else + id + end + end + + private + + def primary_key_scope(scope, id) + if primary_key.is_a? Array + scope.where(primary_key.zip(parse_id(id)).to_h) + else + scope.where(primary_key => id) + end + end + + def bulk_scope(scope, options) + if primary_key.is_a? Array + options[:bulk_ids].map { |id| primary_key_scope(scope, id) }.reduce(&:or) + else + scope.where(primary_key => options[:bulk_ids]) + end + end + + def sort_scope(scope, options) + direction = options[:sort_reverse] ? :asc : :desc + case options[:sort] + when String, Symbol + scope.reorder("#{options[:sort]} #{direction}") + when Array + scope.reorder(options[:sort].zip(Array.new(options[:sort].size) { direction }).to_h) + when Hash + scope.reorder(options[:sort].map { |table_name, column| "#{table_name}.#{column}" }. + zip(Array.new(options[:sort].size) { direction }).to_h) + else + raise ArgumentError.new("Unsupported sort value: #{options[:sort]}") + end + end + class WhereBuilder def initialize(scope) @statements = [] @@ -145,7 +212,7 @@ def query_scope(scope, query, fields = config.list.fields.select(&:queryable?)) # "0055" is the filter index, no use here. o is the operator, v the value def filter_scope(scope, filters, fields = config.list.fields.select(&:filterable?)) filters.each_pair do |field_name, filters_dump| - filters_dump.each do |_, filter_dump| + filters_dump.each_value do |filter_dump| wb = WhereBuilder.new(scope) field = fields.detect { |f| f.name.to_s == field_name } value = parse_field_value(field, filter_dump[:v]) @@ -174,6 +241,8 @@ def unary_operators case @type when :boolean boolean_unary_operators + when :uuid + uuid_unary_operators when :integer, :decimal, :float numeric_unary_operators else @@ -203,9 +272,12 @@ def boolean_unary_operators ) end alias_method :numeric_unary_operators, :boolean_unary_operators + alias_method :uuid_unary_operators, :boolean_unary_operators def range_filter(min, max) - if min && max + if min && max && min == max + ["(#{@column} = ?)", min] + elsif min && max ["(#{@column} BETWEEN ? AND ?)", min, max] elsif min ["(#{@column} >= ?)", min] @@ -218,7 +290,7 @@ def build_statement_for_type case @type when :boolean then build_statement_for_boolean when :integer, :decimal, :float then build_statement_for_integer_decimal_or_float - when :string, :text then build_statement_for_string_or_text + when :string, :text, :citext then build_statement_for_string_or_text when :enum then build_statement_for_enum when :belongs_to_association then build_statement_for_belongs_to_association when :uuid then build_statement_for_uuid @@ -226,8 +298,12 @@ def build_statement_for_type end def build_statement_for_boolean - return ["(#{@column} IS NULL OR #{@column} = ?)", false] if %w(false f 0).include?(@value) - return ["(#{@column} = ?)", true] if %w(true t 1).include?(@value) + case @value + when 'false', 'f', '0' + ["(#{@column} IS NULL OR #{@column} = ?)", false] + when 'true', 't', '1' + ["(#{@column} = ?)", true] + end end def column_for_value(value) @@ -236,6 +312,7 @@ def column_for_value(value) def build_statement_for_belongs_to_association return if @value.blank? + ["(#{@column} = ?)", @value.to_i] if @value.to_i.to_s == @value end @@ -244,13 +321,11 @@ def build_statement_for_string_or_text return ["(#{@column} = ?)", @value] if ['is', '='].include?(@operator) - unless ['postgresql', 'postgis'].include? ar_adapter - @value = @value.mb_chars.downcase - end + @value = @value.mb_chars.downcase unless %w[postgresql postgis].include? ar_adapter - @value = begin + @value = case @operator - when 'default', 'like' + when 'default', 'like', 'not_like' "%#{@value}%" when 'starts_with' "#{@value}%" @@ -259,10 +334,15 @@ def build_statement_for_string_or_text else return end - end - if ['postgresql', 'postgis'].include? ar_adapter - ["(#{@column} ILIKE ?)", @value] + if %w[postgresql postgis].include? ar_adapter + if @operator == 'not_like' + ["(#{@column} NOT ILIKE ?)", @value] + else + ["(#{@column} ILIKE ?)", @value] + end + elsif @operator == 'not_like' + ["(LOWER(#{@column}) NOT LIKE ?)", @value] else ["(LOWER(#{@column}) LIKE ?)", @value] end @@ -270,13 +350,12 @@ def build_statement_for_string_or_text def build_statement_for_enum return if @value.blank? + ["(#{@column} IN (?))", Array.wrap(@value)] end def build_statement_for_uuid - if @value.to_s =~ /\A[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\z/ - column_for_value(@value) - end + column_for_value(@value) if /\A[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\z/.match?(@value.to_s) end def ar_adapter diff --git a/lib/rails_admin/adapters/active_record/abstract_object.rb b/lib/rails_admin/adapters/active_record/abstract_object.rb deleted file mode 100644 index cd9a98cbc0..0000000000 --- a/lib/rails_admin/adapters/active_record/abstract_object.rb +++ /dev/null @@ -1,38 +0,0 @@ -module RailsAdmin - module Adapters - module ActiveRecord - class AbstractObject - # undef almost all of this class's methods so it will pass almost - # everything through to its delegate using method_missing (below). - instance_methods.each { |m| undef_method m unless m.to_s =~ /(^__|^send$|^object_id$)/ } - # ^^^^^ - # the unnecessary "to_s" above is a workaround for meta_where, see - # https://github.com/sferik/rails_admin/issues/374 - - attr_accessor :object - - def initialize(object) - self.object = object - end - - def set_attributes(attributes) - object.assign_attributes(attributes) if attributes - end - - def save(options = {validate: true}) - object.save(**options) - end - - if RUBY_VERSION >= '2.7' - def method_missing(method_name, *args, **kwargs, &block) - object.send(method_name, *args, **kwargs, &block) - end - else - def method_missing(method_name, *args, &block) - object.send(method_name, *args, &block) - end - end - end - end - end -end diff --git a/lib/rails_admin/adapters/active_record/association.rb b/lib/rails_admin/adapters/active_record/association.rb index 4ae72d0551..61f1767948 100644 --- a/lib/rails_admin/adapters/active_record/association.rb +++ b/lib/rails_admin/adapters/active_record/association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Adapters module ActiveRecord @@ -21,24 +23,53 @@ def type association.macro end + def field_type + if polymorphic? + :polymorphic_association + else + :"#{association.macro}_association" + end + end + def klass if options[:polymorphic] - polymorphic_parents(:active_record, model.name.to_s, name) || [] + polymorphic_parents(:active_record, association.active_record.name.to_s, name) || [] else association.klass end end def primary_key - (options[:primary_key] || association.klass.primary_key).try(:to_sym) unless polymorphic? + return nil if polymorphic? + + value = + case type + when :has_one + association.klass.primary_key + else + association.association_primary_key + end + + if value.is_a? Array + :id + else + value.to_sym + end end def foreign_key - association.foreign_key.to_sym + if association.options[:query_constraints].present? + association.options[:query_constraints].map(&:to_sym) + elsif association.foreign_key.is_a?(Array) + association.foreign_key.map(&:to_sym) + else + association.foreign_key.to_sym + end end def foreign_key_nullable? return true if foreign_key.nil? || type != :has_many + (column = klass.columns_hash[foreign_key.to_s]).nil? || column.null end @@ -50,6 +81,21 @@ def foreign_inverse_of nil end + def key_accessor + case type + when :has_many, :has_and_belongs_to_many + :"#{name.to_s.singularize}_ids" + when :has_one + :"#{name}_id" + else + if foreign_key.is_a?(Array) + :"#{name}_id" + else + foreign_key + end + end + end + def as options[:as].try :to_sym end @@ -63,7 +109,7 @@ def inverse_of end def read_only? - (klass.all.instance_eval(&scope).readonly_value if scope.is_a? Proc) || + (klass.all.instance_exec(&scope).readonly_value if scope.is_a?(Proc) && scope.arity == 0) || association.nested? || false end diff --git a/lib/rails_admin/adapters/active_record/object_extension.rb b/lib/rails_admin/adapters/active_record/object_extension.rb new file mode 100644 index 0000000000..a26be2e506 --- /dev/null +++ b/lib/rails_admin/adapters/active_record/object_extension.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module RailsAdmin + module Adapters + module ActiveRecord + module ObjectExtension + def assign_attributes(attributes) + super if attributes + end + end + end + end +end diff --git a/lib/rails_admin/adapters/active_record/property.rb b/lib/rails_admin/adapters/active_record/property.rb index 8c586a2182..fc30c37028 100644 --- a/lib/rails_admin/adapters/active_record/property.rb +++ b/lib/rails_admin/adapters/active_record/property.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Adapters module ActiveRecord @@ -42,13 +44,13 @@ def association? end def read_only? - false + model.readonly_attributes.include? property.name.to_s end private def serialized? - model.type_for_attribute(property.name).class == ::ActiveRecord::Type::Serialized + model.type_for_attribute(property.name).instance_of?(::ActiveRecord::Type::Serialized) end end end diff --git a/lib/rails_admin/adapters/mongoid.rb b/lib/rails_admin/adapters/mongoid.rb index 820cccd06a..52a3870afc 100644 --- a/lib/rails_admin/adapters/mongoid.rb +++ b/lib/rails_admin/adapters/mongoid.rb @@ -1,32 +1,38 @@ +# frozen_string_literal: true + require 'mongoid' require 'rails_admin/config/sections/list' -require 'rails_admin/adapters/mongoid/abstract_object' require 'rails_admin/adapters/mongoid/association' +require 'rails_admin/adapters/mongoid/object_extension' require 'rails_admin/adapters/mongoid/property' require 'rails_admin/adapters/mongoid/bson' module RailsAdmin module Adapters module Mongoid - DISABLED_COLUMN_TYPES = %w(Range Moped::BSON::Binary BSON::Binary Mongoid::Geospatial::Point).freeze + DISABLED_COLUMN_TYPES = %w[Range Moped::BSON::Binary BSON::Binary Mongoid::Geospatial::Point].freeze def parse_object_id(value) Bson.parse_object_id(value) end def new(params = {}) - AbstractObject.new(model.new(params)) + model.new(params).extend(ObjectExtension) end - def get(id) - AbstractObject.new(model.find(id)) - rescue => e - raise e if %w( + def get(id, scope = scoped) + object = scope.find(id) + return nil unless object + + object.extend(ObjectExtension) + rescue StandardError => e + raise e if %w[ Mongoid::Errors::DocumentNotFound Mongoid::Errors::InvalidFind Moped::Errors::InvalidObjectId BSON::InvalidObjectId - ).exclude?(e.class.to_s) + BSON::Error::InvalidObjectId + ].exclude?(e.class.to_s) end def scoped @@ -44,17 +50,15 @@ def all(options = {}, scope = nil) scope = scope.any_in(_id: options[:bulk_ids]) if options[:bulk_ids] scope = query_scope(scope, options[:query]) if options[:query] scope = filter_scope(scope, options[:filters]) if options[:filters] - if options[:page] && options[:per] - scope = scope.send(Kaminari.config.page_method_name, options[:page]).per(options[:per]) - end + scope = scope.send(Kaminari.config.page_method_name, options[:page]).per(options[:per]) if options[:page] && options[:per] scope = sort_by(options, scope) if options[:sort] scope rescue NoMethodError => e - if e.message =~ /page/ - e = e.exception <<-EOM.gsub(/^\s{12}/, '') + if /page/.match?(e.message) + e = e.exception <<~ERROR #{e.message} If you don't have kaminari-mongoid installed, add `gem 'kaminari-mongoid'` to your Gemfile. - EOM + ERROR end raise e end @@ -120,6 +124,7 @@ def make_field_conditions(field, value, operator) collection_name, column_name = parse_collection_name(column_infos[:column]) statement = build_statement(column_name, column_infos[:type], value, operator) next unless statement + conditions_per_collection[collection_name] ||= [] conditions_per_collection[collection_name] << statement end @@ -148,9 +153,10 @@ def filter_scope(scope, filters, fields = config.list.fields.select(&:filterable statements = [] filters.each_pair do |field_name, filters_dump| - filters_dump.each do |_, filter_dump| + filters_dump.each_value do |filter_dump| field = fields.detect { |f| f.name.to_s == field_name } next unless field + value = parse_field_value(field, filter_dump[:v]) conditions_per_collection = make_field_conditions(field, value, (filter_dump[:o] || 'default')) field_statements = make_condition_for_current_collection(field, conditions_per_collection) @@ -191,11 +197,12 @@ def make_condition_for_current_collection(target_field, conditions_per_collectio def perform_search_on_associated_collection(field_name, conditions) target_association = associations.detect { |a| a.name == field_name } return [] unless target_association + model = target_association.klass case target_association.type when :belongs_to, :has_and_belongs_to_many [{target_association.foreign_key.to_s => {'$in' => model.where('$or' => conditions).all.collect { |r| r.send(target_association.primary_key) }}}] - when :has_many + when :has_many, :has_one [{target_association.primary_key.to_s => {'$in' => model.where('$or' => conditions).all.collect { |r| r.send(target_association.foreign_key) }}}] end end @@ -206,9 +213,7 @@ def sort_by(options, scope) case options[:sort] when String field_name, collection_name = options[:sort].split('.').reverse - if collection_name && collection_name != table_name - raise('sorting by associated model column is not supported in Non-Relational databases') - end + raise 'sorting by associated model column is not supported in Non-Relational databases' if collection_name && collection_name != table_name when Symbol field_name = options[:sort].to_s end @@ -246,8 +251,12 @@ def build_statement_for_type end def build_statement_for_boolean - return {@column => false} if %w(false f 0).include?(@value) - return {@column => true} if %w(true t 1).include?(@value) + case @value + when 'false', 'f', '0' + {@column => false} + when 'true', 't', '1' + {@column => true} + end end def column_for_value(value) @@ -256,8 +265,11 @@ def column_for_value(value) def build_statement_for_string_or_text return if @value.blank? - @value = begin + + @value = case @operator + when 'not_like' + Regexp.compile("^((?!#{Regexp.escape(@value)}).)*$", Regexp::IGNORECASE) when 'default', 'like' Regexp.compile(Regexp.escape(@value), Regexp::IGNORECASE) when 'starts_with' @@ -269,12 +281,13 @@ def build_statement_for_string_or_text else return end - end + {@column => @value} end def build_statement_for_enum return if @value.blank? + {@column => {'$in' => Array.wrap(@value)}} end @@ -283,7 +296,9 @@ def build_statement_for_belongs_to_association_or_bson_object_id end def range_filter(min, max) - if min && max + if min && max && min == max + {@column => min} + elsif min && max {@column => {'$gte' => min, '$lte' => max}} elsif min {@column => {'$gte' => min}} diff --git a/lib/rails_admin/adapters/mongoid/abstract_object.rb b/lib/rails_admin/adapters/mongoid/abstract_object.rb deleted file mode 100644 index b02177eb38..0000000000 --- a/lib/rails_admin/adapters/mongoid/abstract_object.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'rails_admin/adapters/active_record/abstract_object' - -module RailsAdmin - module Adapters - module Mongoid - class AbstractObject < RailsAdmin::Adapters::ActiveRecord::AbstractObject - def initialize(object) - super - object.associations.each do |name, association| - association = Association.new(association, object.class) - if [:has_many, :references_many].include? association.macro - instance_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{name.to_s.singularize}_ids - #{name}.map{|item| item.id } - end - - def #{name.to_s.singularize}_ids=(item_ids) - __items__ = Array.wrap(item_ids).map{|item_id| #{name}.klass.find(item_id) rescue nil }.compact - unless persisted? - __items__.each do |item| - item.update_attribute('#{association.foreign_key}', id) - end - end - super __items__.map(&:id) - end -RUBY - elsif [:has_one, :references_one].include? association.macro - instance_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{name}_id=(item_id) - item = (#{association.klass}.find(item_id) rescue nil) - return unless item - item.update_attribute('#{association.foreign_key}', id) unless persisted? - super item.id - end -RUBY - end - end - end - end - end - end -end diff --git a/lib/rails_admin/adapters/mongoid/association.rb b/lib/rails_admin/adapters/mongoid/association.rb index be35b677c3..c30bb31001 100644 --- a/lib/rails_admin/adapters/mongoid/association.rb +++ b/lib/rails_admin/adapters/mongoid/association.rb @@ -1,9 +1,13 @@ +# frozen_string_literal: true + module RailsAdmin module Adapters module Mongoid class Association attr_reader :association, :model + delegate :autosave?, to: :association + def initialize(association, model) @association = association @model = model @@ -28,48 +32,82 @@ def type when :has_and_belongs_to_many, :references_and_referenced_in_many :has_and_belongs_to_many else - raise("Unknown association type: #{macro.inspect}") + raise "Unknown association type: #{macro.inspect}" + end + end + + def field_type + if polymorphic? + :polymorphic_association + else + :"#{type}_association" end end def klass - if polymorphic? && [:referenced_in, :belongs_to].include?(macro) - polymorphic_parents(:mongoid, model.name, name) || [] + if polymorphic? && %i[referenced_in belongs_to].include?(macro) + polymorphic_parents(:mongoid, association.inverse_class_name, name) || [] else association.klass end end def primary_key - association.primary_key.to_sym rescue :_id + case type + when :belongs_to, :has_and_belongs_to_many + association.primary_key.to_sym + else + :_id + end end def foreign_key return if embeds? - association.foreign_key.to_sym rescue nil + + begin + association.foreign_key.to_sym + rescue StandardError + nil + end end def foreign_key_nullable? return if foreign_key.nil? + true end def foreign_type - return unless polymorphic? && [:referenced_in, :belongs_to].include?(macro) + return unless polymorphic? && %i[referenced_in belongs_to].include?(macro) + association.inverse_type.try(:to_sym) || :"#{name}_type" end def foreign_inverse_of - return unless polymorphic? && [:referenced_in, :belongs_to].include?(macro) + return unless polymorphic? && %i[referenced_in belongs_to].include?(macro) + inverse_of_field.try(:to_sym) end + def key_accessor + case macro.to_sym + when :has_many + :"#{name.to_s.singularize}_ids" + when :has_one + :"#{name}_id" + when :embedded_in, :embeds_one, :embeds_many + nil + else + foreign_key + end + end + def as association.as.try :to_sym end def polymorphic? - association.polymorphic? && [:referenced_in, :belongs_to].include?(macro) + association.polymorphic? && %i[referenced_in belongs_to].include?(macro) end def inverse_of @@ -82,13 +120,14 @@ def read_only? def nested_options nested = nested_attributes_options.try { |o| o[name] } - if !nested && [:embeds_one, :embeds_many].include?(macro.to_sym) && !cyclic? - raise <<-MSG.gsub(/^\s+/, '') - Embbeded association without accepts_nested_attributes_for can't be handled by RailsAdmin, - because embedded model doesn't have top-level access. - Please add `accepts_nested_attributes_for :#{association.name}' line to `#{model}' model. + if !nested && %i[embeds_one embeds_many].include?(macro.to_sym) && !cyclic? + raise <<~MSG + Embedded association without accepts_nested_attributes_for can't be handled by RailsAdmin, + because embedded model doesn't have top-level access. + Please add `accepts_nested_attributes_for :#{association.name}' line to `#{model}' model. MSG end + nested end @@ -101,7 +140,7 @@ def macro end def embeds? - [:embeds_one, :embeds_many].include?(macro) + %i[embeds_one embeds_many].include?(macro) end private diff --git a/lib/rails_admin/adapters/mongoid/bson.rb b/lib/rails_admin/adapters/mongoid/bson.rb index d0750992f5..1336d8bb7b 100644 --- a/lib/rails_admin/adapters/mongoid/bson.rb +++ b/lib/rails_admin/adapters/mongoid/bson.rb @@ -1,26 +1,28 @@ +# frozen_string_literal: true + require 'mongoid' module RailsAdmin module Adapters module Mongoid class Bson - OBJECT_ID = begin + OBJECT_ID = if defined?(Moped::BSON) Moped::BSON::ObjectId elsif defined?(BSON::ObjectId) BSON::ObjectId end - end class << self def parse_object_id(value) OBJECT_ID.from_string(value) - rescue => e - raise e if %w( + rescue StandardError => e + raise e if %w[ Moped::Errors::InvalidObjectId BSON::ObjectId::Invalid BSON::InvalidObjectId - ).exclude?(e.class.to_s) + BSON::Error::InvalidObjectId + ].exclude?(e.class.to_s) end end end diff --git a/lib/rails_admin/adapters/mongoid/extension.rb b/lib/rails_admin/adapters/mongoid/extension.rb index 361489cbf6..3803525a24 100644 --- a/lib/rails_admin/adapters/mongoid/extension.rb +++ b/lib/rails_admin/adapters/mongoid/extension.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Adapters module Mongoid diff --git a/lib/rails_admin/adapters/mongoid/object_extension.rb b/lib/rails_admin/adapters/mongoid/object_extension.rb new file mode 100644 index 0000000000..1dea9b890c --- /dev/null +++ b/lib/rails_admin/adapters/mongoid/object_extension.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module RailsAdmin + module Adapters + module Mongoid + module ObjectExtension + def self.extended(object) + object.associations.each do |name, association| + association = Association.new(association, object.class) + case association.macro + when :has_many + unless association.autosave? + object.singleton_class.after_create do + send(name).each(&:save) + end + end + when :has_one + unless association.autosave? + object.singleton_class.after_create do + send(name)&.save + end + end + end + end + end + end + end + end +end diff --git a/lib/rails_admin/adapters/mongoid/property.rb b/lib/rails_admin/adapters/mongoid/property.rb index aded2b67ed..e9ea0c94a6 100644 --- a/lib/rails_admin/adapters/mongoid/property.rb +++ b/lib/rails_admin/adapters/mongoid/property.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module RailsAdmin module Adapters module Mongoid class Property - STRING_TYPE_COLUMN_NAMES = [:name, :title, :subject].freeze + STRING_TYPE_COLUMN_NAMES = %i[name title subject].freeze attr_reader :property, :model def initialize(property, model) @@ -11,11 +13,11 @@ def initialize(property, model) end def name - property.name.to_sym + (property.options[:as] || property.name).to_sym end def pretty_name - property.name.to_s.tr('_', ' ').capitalize + (property.options[:as] || property.name).to_s.tr('_', ' ').capitalize end def type @@ -64,14 +66,14 @@ def association? end def read_only? - false + model.readonly_attributes.include? property.name.to_s end private def object_field_type association = Association.new model.relations.values.detect { |r| r.try(:foreign_key).try(:to_sym) == name }, model - if [:belongs_to, :referenced_in, :embedded_in].include?(association.macro) + if %i[belongs_to referenced_in embedded_in].include?(association.macro) :bson_object_id else :string diff --git a/lib/rails_admin/bootstrap-sass.rb b/lib/rails_admin/bootstrap-sass.rb deleted file mode 100755 index 82fd00730d..0000000000 --- a/lib/rails_admin/bootstrap-sass.rb +++ /dev/null @@ -1,49 +0,0 @@ -module RailsAdmin - module Bootstrap - class FrameworkNotFound < StandardError; end - - # Inspired by Kaminari - def self.load! - if compass? - require 'rails_admin/bootstrap-sass/compass_functions' - register_compass_extension - elsif asset_pipeline? - require 'rails_admin/bootstrap-sass/sass_functions' - end - - require 'sassc-rails' if rails? - - unless rails? || compass? - raise(Bootstrap::FrameworkNotFound.new('bootstrap-sass requires either Rails > 3.1 or Compass, neither of which are loaded')) - end - - if defined?(::Sass) && ::Sass.respond_to?(:load_paths) - stylesheets = File.expand_path(File.join('..', 'vendor', 'assets', 'stylesheets')) - fonts = File.expand_path(File.join('..', 'vendor', 'assets', 'fonts')) - ::Sass.load_paths << stylesheets - ::Sass.load_paths << fonts - end - end - - def self.asset_pipeline? - defined?(::Sprockets) - end - - def self.compass? - defined?(::Compass) - end - - def self.rails? - defined?(::Rails) - end - - def self.register_compass_extension - base = File.join(File.dirname(__FILE__), '..') - styles = File.join(base, 'vendor', 'assets', 'stylesheets') - templates = File.join(base, 'templates') - ::Compass::Frameworks.register('bootstrap', path: base, stylesheets_directory: styles, templates_directory: templates) - end - end -end - -RailsAdmin::Bootstrap.load! diff --git a/lib/rails_admin/bootstrap-sass/compass_functions.rb b/lib/rails_admin/bootstrap-sass/compass_functions.rb deleted file mode 100755 index 3e5ff870fa..0000000000 --- a/lib/rails_admin/bootstrap-sass/compass_functions.rb +++ /dev/null @@ -1,28 +0,0 @@ -module Sass - module Script - module Functions - def image_path(source, _options = {}) - if defined?(::Sprockets) - ::Sass::Script::String.new sprockets_context.image_path(source.value).to_s, :string - elsif defined?(::Compass) - image_url(source, Sass::Script::Bool.new(true)) - else - # Revert to the old compass-agnostic path determination - asset_sans_quotes = source.value.delete('"') - Sass::Script::String.new("/images/#{asset_sans_quotes}", :string) - end - end - - protected - - def sprockets_context # :nodoc: - if options.key?(:sprockets) - options[:sprockets][:context] - else - # Compatibility with sprockets pre 2.10.0 - options[:importer].context - end - end - end - end -end diff --git a/lib/rails_admin/bootstrap-sass/sass_functions.rb b/lib/rails_admin/bootstrap-sass/sass_functions.rb deleted file mode 100755 index 146bc32666..0000000000 --- a/lib/rails_admin/bootstrap-sass/sass_functions.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Sass - module Script - module Functions - # LARS: Snatched from compass - 2011-11-29 - used for gradients in IE6-9 - # returns an IE hex string for a color with an alpha channel - # suitable for passing to IE filters. - def ie_hex_str(color) - assert_type color, :Color - alpha = (color.alpha * 255).round - alphastr = alpha.to_s(16).rjust(2, '0') - Sass::Script::String.new("##{alphastr}#{color.send(:hex_str)[1..-1]}".upcase) - end - declare :ie_hex_str, [:color] if respond_to?(:declare) - end - end -end diff --git a/lib/rails_admin/config.rb b/lib/rails_admin/config.rb index 6254000a90..af557c8ee9 100644 --- a/lib/rails_admin/config.rb +++ b/lib/rails_admin/config.rb @@ -1,5 +1,8 @@ +# frozen_string_literal: true + require 'rails_admin/config/lazy_model' require 'rails_admin/config/sections/list' +require 'rails_admin/support/composite_keys_serializer' require 'active_support/core_ext/module/attribute_accessors' module RailsAdmin @@ -27,15 +30,15 @@ class << self # Configuration option to specify which models you want to exclude. attr_accessor :excluded_models - # Configuration option to specify a whitelist of models you want to RailsAdmin to work with. - # The excluded_models list applies against the whitelist as well and further reduces the models + # Configuration option to specify a allowlist of models you want to RailsAdmin to work with. + # The excluded_models list applies against the allowlist as well and further reduces the models # RailsAdmin will use. # If included_models is left empty ([]), then RailsAdmin will automatically use all the models # in your application (less any excluded_models you may have specified). attr_accessor :included_models # Fields to be hidden in show, create and update views - attr_accessor :default_hidden_fields + attr_reader :default_hidden_fields # Default items per page value used if a model level option has not # been configured @@ -56,14 +59,8 @@ class << self # Tell browsers whether to use the native HTML5 validations (novalidate form option). attr_accessor :browser_validations - # Set the max width of columns in list view before a new set is created - attr_accessor :total_columns_width - - # Enable horizontal-scrolling table in list view, ignore total_columns_width - attr_accessor :sidescroll - # set parent controller - attr_accessor :parent_controller + attr_reader :parent_controller # set settings for `protect_from_forgery` method # By default, it raises exception upon invalid CSRF tokens @@ -75,6 +72,9 @@ class << self # @see RailsAdmin.config attr_reader :registry + # Bootstrap CSS classes used for Navigation bar + attr_accessor :navbar_css_classes + # show Gravatar in Navigation bar attr_accessor :show_gravatar @@ -82,6 +82,12 @@ class << self attr_accessor :navigation_static_links attr_accessor :navigation_static_label + # Set where RailsAdmin fetches JS/CSS from, defaults to :sprockets + attr_writer :asset_source + + # For customization of composite keys representation + attr_accessor :composite_keys_serializer + # Setup authentication to be run as a before filter # This is run inside the controller instance so you can setup any authentication you need to # @@ -110,14 +116,14 @@ def authenticate_with(&blk) @authenticate || DEFAULT_AUTHENTICATION end - # Setup auditing/history/versioning provider that observe objects lifecycle + # Setup auditing/versioning provider that observe objects lifecycle def audit_with(*args, &block) extension = args.shift if extension klass = RailsAdmin::AUDITING_ADAPTERS[extension] klass.setup if klass.respond_to? :setup @audit = proc do - @auditing_adapter = klass.new(*([self] + args).compact) + @auditing_adapter = klass.new(*([self] + args).compact, &block) end elsif block @audit = block @@ -145,7 +151,7 @@ def audit_with(*args, &block) # config.authorize_with :cancancan # end # - # See the wiki[https://github.com/sferik/rails_admin/wiki] for more on authorization. + # See the wiki[https://github.com/railsadminteam/rails_admin/wiki] for more on authorization. # # @see RailsAdmin::Config::DEFAULT_AUTHORIZE def authorize_with(*args, &block) @@ -154,7 +160,7 @@ def authorize_with(*args, &block) klass = RailsAdmin::AUTHORIZATION_ADAPTERS[extension] klass.setup if klass.respond_to? :setup @authorize = proc do - @authorization_adapter = klass.new(*([self] + args).compact) + @authorization_adapter = klass.new(*([self] + args).compact, &block) end elsif block @authorize = block @@ -198,10 +204,10 @@ def current_user_method(&block) end def default_search_operator=(operator) - if %w(default like starts_with ends_with is =).include? operator + if %w[default like not_like starts_with ends_with is =].include? operator @default_search_operator = operator else - raise(ArgumentError.new("Search operator '#{operator}' not supported")) + raise ArgumentError.new("Search operator '#{operator}' not supported") end end @@ -223,23 +229,37 @@ def models_pool # # @see RailsAdmin::Config.registry def model(entity, &block) - key = begin - if entity.is_a?(RailsAdmin::AbstractModel) + key = + case entity + when RailsAdmin::AbstractModel entity.model.try(:name).try :to_sym - elsif entity.is_a?(Class) + when Class, ConstLoadSuppressor::ConstProxy entity.name.to_sym - elsif entity.is_a?(String) || entity.is_a?(Symbol) + when String, Symbol entity.to_sym else entity.class.name.to_sym end - end - @registry[key] ||= RailsAdmin::Config::LazyModel.new(entity) + @registry[key] ||= RailsAdmin::Config::LazyModel.new(key.to_s) @registry[key].add_deferred_block(&block) if block @registry[key] end + def asset_source + @asset_source ||= + begin + detected = defined?(Sprockets) ? :sprockets : :invalid + unless ARGV.join(' ').include? 'rails_admin:install' + warn <<~MSG + [Warning] After upgrading RailsAdmin to 3.x you haven't set asset_source yet, using :#{detected} as the default. + To suppress this message, run 'rails rails_admin:install' to setup the asset delivery method suitable to you. + MSG + end + detected + end + end + def default_hidden_fields=(fields) if fields.is_a?(Array) @default_hidden_fields = {} @@ -250,9 +270,33 @@ def default_hidden_fields=(fields) end end - # Returns action configuration object + def parent_controller=(name) + @parent_controller = name + + if defined?(RailsAdmin::ApplicationController) || defined?(RailsAdmin::MainController) + RailsAdmin::Config::ConstLoadSuppressor.allowing do + RailsAdmin.send(:remove_const, :ApplicationController) + RailsAdmin.send(:remove_const, :MainController) + load RailsAdmin::Engine.root.join('app/controllers/rails_admin/application_controller.rb') + load RailsAdmin::Engine.root.join('app/controllers/rails_admin/main_controller.rb') + end + end + end + + def total_columns_width=(_) + ActiveSupport::Deprecation.warn('The total_columns_width configuration option is deprecated and has no effect.') + end + + def sidescroll=(_) + ActiveSupport::Deprecation.warn('The sidescroll configuration option was removed, it is always enabled now.') + end + + # Setup actions to be used. def actions(&block) - RailsAdmin::Config::Actions.instance_eval(&block) if block + return unless block + + RailsAdmin::Config::Actions.reset + RailsAdmin::Config::Actions.instance_eval(&block) end # Returns all model configurations @@ -274,24 +318,26 @@ def reset @current_user = nil @default_hidden_fields = {} @default_hidden_fields[:base] = [:_type] - @default_hidden_fields[:edit] = [:id, :_id, :created_at, :created_on, :deleted_at, :updated_at, :updated_on, :deleted_on] - @default_hidden_fields[:show] = [:id, :_id, :created_at, :created_on, :deleted_at, :updated_at, :updated_on, :deleted_on] + @default_hidden_fields[:edit] = %i[id _id created_at created_on deleted_at updated_at updated_on deleted_on] + @default_hidden_fields[:show] = %i[id _id created_at created_on deleted_at updated_at updated_on deleted_on] @default_items_per_page = 20 @default_associated_collection_limit = 100 @default_search_operator = 'default' @excluded_models = [] @included_models = [] - @total_columns_width = 697 - @sidescroll = nil - @label_methods = [:name, :title] + @label_methods = %i[name title] @main_app_name = proc { [Rails.application.engine_name.titleize.chomp(' Application'), 'Admin'] } @registry = {} + @navbar_css_classes = %w[navbar-dark bg-primary border-bottom] @show_gravatar = true @navigation_static_links = {} @navigation_static_label = nil + @asset_source = nil + @composite_keys_serializer = RailsAdmin::Support::CompositeKeysSerializer @parent_controller = '::ActionController::Base' @forgery_protection_settings = {with: :exception} RailsAdmin::Config::Actions.reset + RailsAdmin::AbstractModel.reset end # Reset a provided model's configuration. @@ -302,22 +348,19 @@ def reset_model(model) @registry.delete(key) end - # Reset all models configuration - # Used to clear all configurations when reloading code in development. - # @see RailsAdmin::Engine - # @see RailsAdmin::Config.registry - def reset_all_models - @registry = {} + # Perform reset, then load RailsAdmin initializer again + def reload! + reset + load RailsAdmin::Engine.config.initializer_path end # Get all models that are configured as visible sorted by their weight and label. # # @see RailsAdmin::Config::Hideable - def visible_models(bindings) visible_models_with_bindings(bindings).sort do |a, b| if (weight_order = a.weight <=> b.weight) == 0 - a.label.downcase <=> b.label.downcase + a.label.casecmp(b.label) else weight_order end @@ -326,23 +369,22 @@ def visible_models(bindings) private - def lchomp(base, arg) - base.to_s.reverse.chomp(arg.to_s.reverse).reverse - end - def viable_models included_models.collect(&:to_s).presence || begin @@system_models ||= # memoization for tests ([Rails.application] + Rails::Engine.subclasses.collect(&:instance)).flat_map do |app| - (app.paths['app/models'].to_a + app.paths.eager_load).collect do |load_path| + (app.paths['app/models'].to_a + app.config.eager_load_paths).collect do |load_path| Dir.glob(app.root.join(load_path)).collect do |load_dir| - Dir.glob(load_dir + '/**/*.rb').collect do |filename| + path_prefix = "#{app.root.join(load_dir)}/" + Dir.glob("#{load_dir}/**/*.rb").collect do |filename| # app/models/module/class.rb => module/class.rb => module/class => Module::Class - lchomp(filename, "#{app.root.join(load_dir)}/").chomp('.rb').camelize + filename.delete_prefix(path_prefix).chomp('.rb').camelize end end end end.flatten.reject { |m| m.starts_with?('Concerns::') } # rubocop:disable Style/MultilineBlockChain + + @@system_models + @registry.keys.collect(&:to_s) end end diff --git a/lib/rails_admin/config/actions.rb b/lib/rails_admin/config/actions.rb index 6c4f33d49a..8bdbc20b24 100644 --- a/lib/rails_admin/config/actions.rb +++ b/lib/rails_admin/config/actions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Actions @@ -9,7 +11,7 @@ def all(scope = nil, bindings = {}) end scope ||= :all init_actions! - actions = begin + actions = case scope when :all @@actions @@ -22,7 +24,7 @@ def all(scope = nil, bindings = {}) when :member @@actions.select(&:member?) end - end + actions = actions.collect { |action| action.with(bindings) } bindings[:controller] ? actions.select(&:visible?) : actions end @@ -52,7 +54,7 @@ def add_action(key, parent_class, parent, &block) def key :#{key} end - )) + ), __FILE__, __LINE__ - 5) add_action_custom_key(a, &block) end @@ -71,7 +73,7 @@ def #{name}(&block) action = #{klass}.new add_action_custom_key(action, &block) end - } + }, __FILE__, __LINE__ - 5 end private diff --git a/lib/rails_admin/config/actions/base.rb b/lib/rails_admin/config/actions/base.rb index c604ea6c57..f9a681cd1e 100644 --- a/lib/rails_admin/config/actions/base.rb +++ b/lib/rails_admin/config/actions/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/proxyable' require 'rails_admin/config/configurable' require 'rails_admin/config/hideable' @@ -36,7 +38,7 @@ class Base # http://getbootstrap.com/2.3.2/base-css.html#icons register_instance_option :link_icon do - 'icon-question-sign' + 'fas fa-question' end # Should the action be visible @@ -49,7 +51,7 @@ class Base (only.nil? || [only].flatten.collect(&:to_s).include?(bindings[:abstract_model].to_s)) && ![except].flatten.collect(&:to_s).include?(bindings[:abstract_model].to_s) && !bindings[:abstract_model].config.excluded? - ) + ) && (!respond_to?(:writable?) || writable?) end register_instance_option :authorized? do @@ -73,8 +75,13 @@ class Base false end - # Render via pjax? - register_instance_option :pjax? do + # Target window [_self, _blank] + register_instance_option :link_target do + nil + end + + # Determines whether to navigate via Turbo Drive or not + register_instance_option :turbo? do true end diff --git a/lib/rails_admin/config/actions/bulk_delete.rb b/lib/rails_admin/config/actions/bulk_delete.rb index a2aa821272..b8713eeee1 100644 --- a/lib/rails_admin/config/actions/bulk_delete.rb +++ b/lib/rails_admin/config/actions/bulk_delete.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Actions @@ -9,7 +11,7 @@ class BulkDelete < RailsAdmin::Config::Actions::Base end register_instance_option :http_methods do - [:post, :delete] + %i[post delete] end register_instance_option :controller do @@ -36,7 +38,7 @@ class BulkDelete < RailsAdmin::Config::Actions::Base destroyed = processed_objects.select(&:destroyed?) not_destroyed = processed_objects - destroyed destroyed.each do |object| - @auditing_adapter && @auditing_adapter.delete_object(object, @abstract_model, _current_user) + @auditing_adapter&.delete_object(object, @abstract_model, _current_user) end end end diff --git a/lib/rails_admin/config/actions/dashboard.rb b/lib/rails_admin/config/actions/dashboard.rb index f3cd666ab3..584b897b2d 100644 --- a/lib/rails_admin/config/actions/dashboard.rb +++ b/lib/rails_admin/config/actions/dashboard.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Actions @@ -18,20 +20,21 @@ class Dashboard < RailsAdmin::Config::Actions::Base register_instance_option :controller do proc do - @history = @auditing_adapter && @auditing_adapter.latest(@action.auditing_versions_limit) || [] + @history = @auditing_adapter&.latest(@action.auditing_versions_limit) if @action.history? if @action.statistics? - @abstract_models = RailsAdmin::Config.visible_models(controller: self).collect(&:abstract_model) + model_configs = RailsAdmin::Config.visible_models(controller: self) + @abstract_models = model_configs.map(&:abstract_model) @most_recent_created = {} @count = {} @max = 0 - @abstract_models.each do |t| - scope = @authorization_adapter && @authorization_adapter.query(:index, t) - current_count = t.count({}, scope) + model_configs.each do |config| + scope = @authorization_adapter&.query(:index, config.abstract_model) + current_count = config.abstract_model.count({}, scope) @max = current_count > @max ? current_count : @max - @count[t.model.name] = current_count - next unless t.properties.detect { |c| c.name == :created_at } - @most_recent_created[t.model.name] = t.model.last.try(:created_at) + name = config.abstract_model.model.name + @count[name] = current_count + @most_recent_created[name] = config.last_created_at end end render @action.template_name, status: @status_code || :ok @@ -43,12 +46,16 @@ class Dashboard < RailsAdmin::Config::Actions::Base end register_instance_option :link_icon do - 'icon-home' + 'fas fa-home' end register_instance_option :statistics? do true end + + register_instance_option :history? do + true + end end end end diff --git a/lib/rails_admin/config/actions/delete.rb b/lib/rails_admin/config/actions/delete.rb index a47ac26968..29da7c8af6 100644 --- a/lib/rails_admin/config/actions/delete.rb +++ b/lib/rails_admin/config/actions/delete.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Actions @@ -13,7 +15,7 @@ class Delete < RailsAdmin::Config::Actions::Base end register_instance_option :http_methods do - [:get, :delete] + %i[get delete] end register_instance_option :authorization_key do @@ -31,24 +33,24 @@ class Delete < RailsAdmin::Config::Actions::Base elsif request.delete? # DESTROY - redirect_path = nil - @auditing_adapter && @auditing_adapter.delete_object(@object, @abstract_model, _current_user) + @auditing_adapter&.delete_object(@object, @abstract_model, _current_user) if @object.destroy flash[:success] = t('admin.flash.successful', name: @model_config.label, action: t('admin.actions.delete.done')) - redirect_path = index_path + redirect_to index_path else - flash[:error] = t('admin.flash.error', name: @model_config.label, action: t('admin.actions.delete.done')) - redirect_path = back_or_index + handle_save_error :delete end - redirect_to redirect_path - end end end register_instance_option :link_icon do - 'icon-remove' + 'fas fa-times' + end + + register_instance_option :writable? do + !(bindings[:object] && bindings[:object].readonly?) end end end diff --git a/lib/rails_admin/config/actions/edit.rb b/lib/rails_admin/config/actions/edit.rb index ab6a3d290c..316d8e4796 100644 --- a/lib/rails_admin/config/actions/edit.rb +++ b/lib/rails_admin/config/actions/edit.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Actions @@ -9,7 +11,7 @@ class Edit < RailsAdmin::Config::Actions::Base end register_instance_option :http_methods do - [:get, :put] + %i[get put] end register_instance_option :controller do @@ -18,20 +20,20 @@ class Edit < RailsAdmin::Config::Actions::Base respond_to do |format| format.html { render @action.template_name } - format.js { render @action.template_name, layout: false } + format.js { render @action.template_name, layout: 'rails_admin/modal', content_type: Mime[:html].to_s } end elsif request.put? # UPDATE sanitize_params_for!(request.xhr? ? :modal : :update) - @object.set_attributes(params[@abstract_model.param_key].to_h) - @authorization_adapter && @authorization_adapter.authorize(:update, @abstract_model, @object) + @object.assign_attributes(params[@abstract_model.param_key]) + @authorization_adapter&.authorize(:update, @abstract_model, @object) changes = @object.changes if @object.save - @auditing_adapter && @auditing_adapter.update_object(@object, @abstract_model, _current_user, changes) + @auditing_adapter&.update_object(@object, @abstract_model, _current_user, changes) respond_to do |format| format.html { redirect_to_on_success } - format.js { render json: {id: @object.id.to_s, label: @model_config.with(object: @object).object_label} } + format.json { render json: {id: @object.id.to_s, label: @model_config.with(object: @object).object_label} } end else handle_save_error :edit @@ -42,7 +44,11 @@ class Edit < RailsAdmin::Config::Actions::Base end register_instance_option :link_icon do - 'icon-pencil' + 'fas fa-pencil-alt' + end + + register_instance_option :writable? do + !(bindings[:object] && bindings[:object].readonly?) end end end diff --git a/lib/rails_admin/config/actions/export.rb b/lib/rails_admin/config/actions/export.rb index d115db9200..85ced07c58 100644 --- a/lib/rails_admin/config/actions/export.rb +++ b/lib/rails_admin/config/actions/export.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Actions @@ -9,12 +11,13 @@ class Export < RailsAdmin::Config::Actions::Base end register_instance_option :http_methods do - [:get, :post] + %i[get post] end register_instance_option :controller do proc do - if format = params[:json] && :json || params[:csv] && :csv || params[:xml] && :xml + format = params[:json] && :json || params[:csv] && :csv || params[:xml] && :xml + if format request.format = format @schema = HashHelper.symbolize(params[:schema].slice(:except, :include, :methods, :only).permit!.to_h) if params[:schema] # to_json and to_xml expect symbols for keys AND values. @objects = list_entries(@model_config, :export) @@ -30,7 +33,7 @@ class Export < RailsAdmin::Config::Actions::Base end register_instance_option :link_icon do - 'icon-share' + 'fas fa-file-export' end end end diff --git a/lib/rails_admin/config/actions/history_index.rb b/lib/rails_admin/config/actions/history_index.rb index 6f1fe54d6f..835f9d27dd 100644 --- a/lib/rails_admin/config/actions/history_index.rb +++ b/lib/rails_admin/config/actions/history_index.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Actions @@ -19,7 +21,7 @@ class HistoryIndex < RailsAdmin::Config::Actions::Base register_instance_option :controller do proc do @general = true - @history = @auditing_adapter && @auditing_adapter.listing_for_model(@abstract_model, params[:query], params[:sort], params[:sort_reverse], params[:all], params[:page]) || [] + @history = @auditing_adapter&.listing_for_model(@abstract_model, params[:query], params[:sort], params[:sort_reverse], params[:all], params[Kaminari.config.param_name]) || [] render @action.template_name end @@ -30,7 +32,7 @@ class HistoryIndex < RailsAdmin::Config::Actions::Base end register_instance_option :link_icon do - 'icon-book' + 'fas fa-book' end end end diff --git a/lib/rails_admin/config/actions/history_show.rb b/lib/rails_admin/config/actions/history_show.rb index e78bb3f11d..9975481f14 100644 --- a/lib/rails_admin/config/actions/history_show.rb +++ b/lib/rails_admin/config/actions/history_show.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Actions @@ -19,7 +21,7 @@ class HistoryShow < RailsAdmin::Config::Actions::Base register_instance_option :controller do proc do @general = false - @history = @auditing_adapter && @auditing_adapter.listing_for_object(@abstract_model, @object, params[:query], params[:sort], params[:sort_reverse], params[:all], params[:page]) || [] + @history = @auditing_adapter&.listing_for_object(@abstract_model, @object, params[:query], params[:sort], params[:sort_reverse], params[:all], params[Kaminari.config.param_name]) || [] render @action.template_name end @@ -30,7 +32,7 @@ class HistoryShow < RailsAdmin::Config::Actions::Base end register_instance_option :link_icon do - 'icon-book' + 'fas fa-book' end end end diff --git a/lib/rails_admin/config/actions/index.rb b/lib/rails_admin/config/actions/index.rb index 131ad1784a..366a15b57c 100644 --- a/lib/rails_admin/config/actions/index.rb +++ b/lib/rails_admin/config/actions/index.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'activemodel-serializers-xml' module RailsAdmin @@ -11,7 +13,7 @@ class Index < RailsAdmin::Config::Actions::Base end register_instance_option :http_methods do - [:get, :post] + %i[get post] end register_instance_option :route_fragment do @@ -20,7 +22,8 @@ class Index < RailsAdmin::Config::Actions::Base register_instance_option :breadcrumb_parent do parent_model = bindings[:abstract_model].try(:config).try(:parent) - if am = parent_model && RailsAdmin.config(parent_model).try(:abstract_model) + am = parent_model && RailsAdmin.config(parent_model).try(:abstract_model) + if am [:index, am] else [:dashboard] @@ -33,9 +36,7 @@ class Index < RailsAdmin::Config::Actions::Base unless @model_config.list.scopes.empty? if params[:scope].blank? - unless @model_config.list.scopes.first.nil? - @objects = @objects.send(@model_config.list.scopes.first) - end + @objects = @objects.send(@model_config.list.scopes.first) unless @model_config.list.scopes.first.nil? elsif @model_config.list.scopes.collect(&:to_s).include?(params[:scope]) @objects = @objects.send(params[:scope].to_sym) end @@ -47,15 +48,17 @@ class Index < RailsAdmin::Config::Actions::Base end format.json do - output = begin + output = if params[:compact] - primary_key_method = @association ? @association.associated_primary_key : @model_config.abstract_model.primary_key - label_method = @model_config.object_label_method - @objects.collect { |o| {id: o.send(primary_key_method).to_s, label: o.send(label_method).to_s} } + if @association + @association.collection(@objects).collect { |(label, id)| {id: id, label: label} } + else + @objects.collect { |object| {id: object.id.to_s, label: object.send(@model_config.object_label_method).to_s} } + end else @objects.to_json(@schema) end - end + if params[:send_data] send_data output, filename: "#{params[:model_name]}_#{DateTime.now.strftime('%Y-%m-%d_%Hh%Mm%S')}.json" else @@ -87,7 +90,7 @@ class Index < RailsAdmin::Config::Actions::Base end register_instance_option :link_icon do - 'icon-th-list' + 'fas fa-th-list' end end end diff --git a/lib/rails_admin/config/actions/new.rb b/lib/rails_admin/config/actions/new.rb index 84124c3b28..94a868e610 100644 --- a/lib/rails_admin/config/actions/new.rb +++ b/lib/rails_admin/config/actions/new.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Actions @@ -9,7 +11,7 @@ class New < RailsAdmin::Config::Actions::Base end register_instance_option :http_methods do - [:get, :post] # NEW / CREATE + %i[get post] # NEW / CREATE end register_instance_option :controller do @@ -17,16 +19,18 @@ class New < RailsAdmin::Config::Actions::Base if request.get? # NEW @object = @abstract_model.new - @authorization_adapter && @authorization_adapter.attributes_for(:new, @abstract_model).each do |name, value| + @action = @action.with(@action.bindings.merge(object: @object)) + @authorization_adapter&.attributes_for(:new, @abstract_model)&.each do |name, value| @object.send("#{name}=", value) end - if object_params = params[@abstract_model.param_key] + object_params = params[@abstract_model.param_key] + if object_params sanitize_params_for!(request.xhr? ? :modal : :create) - @object.set_attributes(@object.attributes.merge(object_params.to_h)) + @object.assign_attributes(@object.attributes.merge(object_params.to_h)) end respond_to do |format| format.html { render @action.template_name } - format.js { render @action.template_name, layout: false } + format.js { render @action.template_name, layout: 'rails_admin/modal', content_type: Mime[:html].to_s } end elsif request.post? # CREATE @@ -35,14 +39,14 @@ class New < RailsAdmin::Config::Actions::Base @object = @abstract_model.new sanitize_params_for!(request.xhr? ? :modal : :create) - @object.set_attributes(params[@abstract_model.param_key].to_h) - @authorization_adapter && @authorization_adapter.authorize(:create, @abstract_model, @object) + @object.assign_attributes(params[@abstract_model.param_key]) + @authorization_adapter&.authorize(:create, @abstract_model, @object) if @object.save - @auditing_adapter && @auditing_adapter.create_object(@object, @abstract_model, _current_user) + @auditing_adapter&.create_object(@object, @abstract_model, _current_user) respond_to do |format| format.html { redirect_to_on_success } - format.js { render json: {id: @object.id.to_s, label: @model_config.with(object: @object).object_label} } + format.json { render json: {id: @object.id.to_s, label: @model_config.with(object: @object).object_label} } end else handle_save_error @@ -53,7 +57,11 @@ class New < RailsAdmin::Config::Actions::Base end register_instance_option :link_icon do - 'icon-plus' + 'fas fa-plus' + end + + register_instance_option :writable? do + !(bindings[:object] && bindings[:object].readonly?) end end end diff --git a/lib/rails_admin/config/actions/show.rb b/lib/rails_admin/config/actions/show.rb index ce386ab2ee..18d587399e 100644 --- a/lib/rails_admin/config/actions/show.rb +++ b/lib/rails_admin/config/actions/show.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Actions @@ -26,7 +28,7 @@ class Show < RailsAdmin::Config::Actions::Base end register_instance_option :link_icon do - 'icon-info-sign' + 'fas fa-info-circle' end end end diff --git a/lib/rails_admin/config/actions/show_in_app.rb b/lib/rails_admin/config/actions/show_in_app.rb index 5a4f6f23a5..bb238f5122 100644 --- a/lib/rails_admin/config/actions/show_in_app.rb +++ b/lib/rails_admin/config/actions/show_in_app.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Actions @@ -9,7 +11,11 @@ class ShowInApp < RailsAdmin::Config::Actions::Base end register_instance_option :visible? do - authorized? && (bindings[:controller].main_app.url_for(bindings[:object]) rescue false) + authorized? && begin + bindings[:controller].main_app.url_for(bindings[:object]) + rescue StandardError + false + end end register_instance_option :controller do @@ -19,10 +25,10 @@ class ShowInApp < RailsAdmin::Config::Actions::Base end register_instance_option :link_icon do - 'icon-eye-open' + 'fas fa-eye' end - register_instance_option :pjax? do + register_instance_option :turbo? do false end end diff --git a/lib/rails_admin/config/configurable.rb b/lib/rails_admin/config/configurable.rb index ad4f52c55b..382677f2ee 100644 --- a/lib/rails_admin/config/configurable.rb +++ b/lib/rails_admin/config/configurable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config # A module for all configurables. @@ -9,7 +11,7 @@ def self.included(base) def has_option?(name) # rubocop:disable Naming/PredicateName options = self.class.instance_variable_get('@config_options') - options && options.key?(name) + options&.key?(name) end # Register an instance option for this object only @@ -87,7 +89,7 @@ def register_deprecated_instance_option(option_name, replacement_option_name = n elsif block_given? yield else - raise("The #{option_name} configuration option is removed without replacement.") + raise "The #{option_name} configuration option is removed without replacement." end end end diff --git a/lib/rails_admin/config/const_load_suppressor.rb b/lib/rails_admin/config/const_load_suppressor.rb new file mode 100644 index 0000000000..065fd8f252 --- /dev/null +++ b/lib/rails_admin/config/const_load_suppressor.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module RailsAdmin + module Config + module ConstLoadSuppressor + class << self + @original_const_missing = nil + + def suppressing + raise 'Constant Loading is already suppressed' if @original_const_missing + + begin + @original_const_missing = Object.method(:const_missing) + intercept_const_missing + yield + ensure + Object.define_singleton_method(:const_missing, @original_const_missing) + @original_const_missing = nil + end + end + + def allowing + if @original_const_missing + begin + Object.define_singleton_method(:const_missing, @original_const_missing) + yield + ensure + intercept_const_missing + end + else + yield + end + end + + private + + def intercept_const_missing + Object.define_singleton_method(:const_missing) do |name| + ConstProxy.new(name.to_s) + end + end + end + + class ConstProxy < BasicObject + attr_reader :name + + def initialize(name) + @name = name + end + + def klass + @klass ||= + begin + unless ::Object.const_defined?(name) + ::Kernel.raise <<~MESSAGE + The constant #{name} is not loaded yet upon the execution of the RailsAdmin initializer. + We don't recommend to do this and may lead to issues, but if you really have to do so you can explicitly require it by adding: + + require '#{name.underscore}' + + on top of config/initializers/rails_admin.rb. + MESSAGE + end + name.constantize + end + end + + def method_missing(method_name, *args, &block) + klass.send(method_name, *args, &block) + end + + def respond_to_missing?(method_name, include_private = false) + super || klass.respond_to?(method_name, include_private) + end + end + end + end +end diff --git a/lib/rails_admin/config/fields.rb b/lib/rails_admin/config/fields.rb index 5c5b0d8978..907e5e2e16 100644 --- a/lib/rails_admin/config/fields.rb +++ b/lib/rails_admin/config/fields.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Fields @@ -20,7 +22,7 @@ module Fields # Registry of field factories. # - # Field factory is an anonymous function that recieves the parent object, + # Field factory is an anonymous function that receives the parent object, # an array of field properties and an array of fields already instantiated. # # If the factory returns true then that property will not be run through @@ -50,13 +52,15 @@ def self.factory(parent) parent.abstract_model.properties.each do |properties| # Unless a previous factory has already loaded current field as well next if fields.detect { |f| f.name == properties.name } + # Loop through factories until one returns true @@registry.detect { |factory| factory.call(parent, properties, fields) } end # Load fields for all associations (relations) - parent.abstract_model.associations.select { |a| a.type != :belongs_to }.each do |association| # :belongs_to are created by factory for belongs_to fields + parent.abstract_model.associations.reject { |a| a.type == :belongs_to }.each do |association| # :belongs_to are created by factory for belongs_to fields # Unless a previous factory has already loaded current field as well next if fields.detect { |f| f.name == association.name } + # Loop through factories until one returns true @@registry.detect { |factory| factory.call(parent, association, fields) } end diff --git a/lib/rails_admin/config/fields/association.rb b/lib/rails_admin/config/fields/association.rb index d327e2a7dd..b5d8194cad 100644 --- a/lib/rails_admin/config/fields/association.rb +++ b/lib/rails_admin/config/fields/association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config' require 'rails_admin/config/fields/base' @@ -5,15 +7,15 @@ module RailsAdmin module Config module Fields class Association < RailsAdmin::Config::Fields::Base - def self.inherited(klass) - super(klass) - end - # Reader for the association information hash def association @properties end + def method_name + nested_form ? :"#{name}_attributes" : association.key_accessor + end + register_instance_option :pretty_value do v = bindings[:view] [value].flatten.select(&:present?).collect do |associated| @@ -21,8 +23,8 @@ def association am = amc.abstract_model wording = associated.send(amc.object_label_method) can_see = !am.embedded? && (show_action = v.action(:show, am, associated)) - can_see ? v.link_to(wording, v.url_for(action: show_action.action_name, model_name: am.to_param, id: associated.id), class: 'pjax') : ERB::Util.html_escape(wording) - end.to_sentence.html_safe + can_see ? v.link_to(wording, v.url_for(action: show_action.action_name, model_name: am.to_param, id: associated.id)) : ERB::Util.html_escape(wording) + end.to_sentence.html_safe.presence || '-' end # Accessor whether association is visible or not. By default @@ -54,7 +56,30 @@ def association # preload entire associated collection (per associated_collection_scope) on load # Be sure to set limit in associated_collection_scope if set is large register_instance_option :associated_collection_cache_all do - @associated_collection_cache_all ||= (associated_model_config.abstract_model.count < associated_model_limit) + @associated_collection_cache_all ||= dynamically_scope_by.blank? && (associated_model_config.abstract_model.count < associated_model_limit) + end + + # client-side dynamic scoping + register_instance_option :dynamically_scope_by do + nil + end + + # parses #dynamically_scope_by and returns a Hash in the form of + # {[form field name in this model]: [field name in the associated model]} + def dynamic_scope_relationships + @dynamic_scope_relationships ||= + Array.wrap(dynamically_scope_by).flat_map do |field| + field.is_a?(Hash) ? field.to_a : [[field, field]] + end.map do |field_name, target_name| # rubocop:disable Style/MultilineBlockChain + field = section.fields.detect { |f| f.name == field_name } + raise "Field '#{field_name}' was given for #dynamically_scope_by but not found in '#{abstract_model.model_name}'" unless field + + target_field = associated_model_config.list.fields.detect { |f| f.name == target_name } + raise "Field '#{field_name}' was given for #dynamically_scope_by but not found in '#{associated_model_config.abstract_model.model_name}'" unless target_field + raise "Field '#{field_name}' in '#{associated_model_config.abstract_model.model_name}' can't be used for dynamic scoping because it's not filterable" unless target_field.filterable + + [field.method_name, target_name] + end.to_h end # determines whether association's elements can be removed @@ -62,10 +87,18 @@ def association association.foreign_key_nullable? end - register_instance_option :eager_load? do + register_instance_option :eager_load do !!searchable end + register_instance_option :inline_add do + true + end + + register_instance_option :inline_edit do + true + end + # Reader for the association's child model's configuration def associated_model_config @associated_model_config ||= RailsAdmin.config(association.klass) @@ -78,12 +111,12 @@ def associated_object_label_method # Reader for associated primary key def associated_primary_key - @associated_primary_key ||= associated_model_config.abstract_model.primary_key + association.primary_key end - # Reader for the association's key - def foreign_key - association.foreign_key + # Returns params which are to be set in modals + def associated_prepopulate_params + {} end # Reader whether this is a polymorphic association @@ -101,6 +134,12 @@ def value bindings[:object].send(association.name) end + # Returns collection of all selectable records + def collection(scope = nil) + (scope || bindings[:controller].list_entries(associated_model_config, :index, associated_collection_scope, false)). + map { |o| [o.send(associated_object_label_method), format_key(o.send(associated_primary_key)).to_s] } + end + # has many? def multiple? true @@ -113,6 +152,16 @@ def virtual? def associated_model_limit RailsAdmin.config.default_associated_collection_limit end + + private + + def format_key(key) + if key.is_a?(Array) + RailsAdmin.config.composite_keys_serializer.serialize(key) + else + key + end + end end end end diff --git a/lib/rails_admin/config/fields/base.rb b/lib/rails_admin/config/fields/base.rb index e3002eaf28..184bc499d6 100644 --- a/lib/rails_admin/config/fields/base.rb +++ b/lib/rails_admin/config/fields/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/proxyable' require 'rails_admin/config/configurable' require 'rails_admin/config/hideable' @@ -14,13 +16,12 @@ class Base # rubocop:disable Metrics/ClassLength include RailsAdmin::Config::Groupable include RailsAdmin::Config::Inspectable - attr_reader :name, :properties, :abstract_model + attr_reader :name, :properties, :abstract_model, :parent, :root attr_accessor :defined, :order, :section - attr_reader :parent, :root - NAMED_INSTANCE_VARIABLES = [ - :@parent, :@root, :@section, :@children_fields_registered, - :@associated_model_config, :@group + NAMED_INSTANCE_VARIABLES = %i[ + @parent @root @section @children_fields_registered + @associated_model_config @group ].freeze def initialize(parent, name, properties) @@ -36,7 +37,7 @@ def initialize(parent, name, properties) end register_instance_option :css_class do - "#{self.name}_field" + "#{name}_field" end def type_css_class @@ -51,14 +52,36 @@ def virtual? nil end + register_instance_option :sticky? do + false + end + register_instance_option :sortable do !virtual? || children_fields.first || false end + def sort_column + if sortable == true + "#{abstract_model.quoted_table_name}.#{abstract_model.quote_column_name(name)}" + elsif (sortable.is_a?(String) || sortable.is_a?(Symbol)) && sortable.to_s.include?('.') # just provide sortable, don't do anything smart + sortable + elsif sortable.is_a?(Hash) # just join sortable hash, don't do anything smart + "#{sortable.keys.first}.#{sortable.values.first}" + elsif association? # use column on target table + "#{associated_model_config.abstract_model.quoted_table_name}.#{abstract_model.quote_column_name(sortable)}" + else # use described column in the field conf. + "#{abstract_model.quoted_table_name}.#{abstract_model.quote_column_name(sortable)}" + end + end + register_instance_option :searchable do !virtual? || children_fields.first || false end + register_instance_option :search_operator do + RailsAdmin::Config.default_search_operator + end + register_instance_option :queryable? do !virtual? end @@ -67,8 +90,22 @@ def virtual? !!searchable end - register_instance_option :search_operator do - @search_operator ||= RailsAdmin::Config.default_search_operator + register_instance_option :filter_operators do + [] + end + + register_instance_option :default_filter_operator do + nil + end + + def filter_options + { + label: label, + name: name, + operator: default_filter_operator, + operators: filter_operators, + type: type, + } end # serials and dates are reversed in list, which is more natural (last modified items first). @@ -78,7 +115,7 @@ def virtual? # list of columns I should search for that field [{ column: 'table_name.column', type: field.type }, {..}] register_instance_option :searchable_columns do - @searchable_columns ||= begin + @searchable_columns ||= case searchable when true [{column: "#{abstract_model.table_name}.#{name}", type: type}] @@ -93,22 +130,21 @@ def virtual? table_name, column = f.split '.' type = nil elsif f.is_a?(Hash) # => - am = f.keys.first.is_a?(Class) && AbstractModel.new(f.keys.first) - table_name = am && am.table_name || f.keys.first + am = AbstractModel.new(f.keys.first) if f.keys.first.is_a?(Class) + table_name = am&.table_name || f.keys.first column = f.values.first - property = am && am.properties.detect { |p| p.name == f.values.first.to_sym } - type = property && property.type + property = am&.properties&.detect { |p| p.name == f.values.first.to_sym } + type = property&.type else # am = (association? ? associated_model_config.abstract_model : abstract_model) table_name = am.table_name column = f property = am.properties.detect { |p| p.name == f.to_sym } - type = property && property.type + type = property&.type end {column: "#{table_name}.#{column}", type: (type || :string)} end end - end end register_instance_option :formatted_value do @@ -155,7 +191,7 @@ def virtual? # # @see RailsAdmin::AbstractModel.properties register_instance_option :length do - @length ||= properties && properties.length + @length ||= properties&.length end # Accessor for field's length restrictions per validations @@ -172,17 +208,17 @@ def virtual? # # @see RailsAdmin::AbstractModel.properties register_instance_option :required? do - context = begin + context = if bindings && bindings[:object] bindings[:object].persisted? ? :update : :create else :nil end - end + (@required ||= {})[context] ||= !!([name] + children_fields).uniq.detect do |column_name| abstract_model.model.validators_on(column_name).detect do |v| !(v.options[:allow_nil] || v.options[:allow_blank]) && - [:presence, :numericality, :attachment_presence].include?(v.kind) && + %i[presence numericality attachment_presence].include?(v.kind) && (v.options[:on] == context || v.options[:on].blank?) && (v.options[:if].blank? && v.options[:unless].blank?) end @@ -193,7 +229,7 @@ def virtual? # # @see RailsAdmin::AbstractModel.properties register_instance_option :serial? do - properties && properties.serial? + properties&.serial? end register_instance_option :view_helper do @@ -213,6 +249,7 @@ def virtual? returned = true (RailsAdmin.config.default_hidden_fields || {}).each do |section, fields| next unless self.section.is_a?("RailsAdmin::Config::Sections::#{section.to_s.camelize}".constantize) + returned = false if fields.include?(name) end returned @@ -223,16 +260,29 @@ def virtual? [] end - register_instance_option :render do - bindings[:view].render partial: "rails_admin/main/#{partial}", locals: {field: self, form: bindings[:form]} + register_instance_option :eager_load do + false end - register_instance_option :default_filter_operator do - nil + register_deprecated_instance_option :eager_load?, :eager_load + + def eager_load_values + case eager_load + when true + [name] + when false, nil + [] + else + Array.wrap(eager_load) + end + end + + register_instance_option :render do + bindings[:view].render partial: "rails_admin/main/#{partial}", locals: {field: self, form: bindings[:form]} end def editable? - !(@properties && @properties.read_only?) + !((@properties && @properties.read_only?) || (bindings[:object] && bindings[:object].readonly?)) end # Is this an association @@ -259,7 +309,7 @@ def optional? # @see RailsAdmin::Config::Fields::Base.register_instance_option :required? def optional(state = nil, &block) if !state.nil? || block - required state.nil? ? proc { false == instance_eval(&block) } : false == state + required state.nil? ? proc { instance_eval(&block) == false } : state == false else optional? end @@ -281,14 +331,14 @@ def type def value bindings[:object].safe_send(name) rescue NoMethodError => e - raise e.exception <<-EOM.gsub(/^\s{10}/, '') - #{e.message} - If you want to use a RailsAdmin virtual field(= a field without corresponding instance method), - you should declare 'formatted_value' in the field definition. - field :#{name} do - formatted_value{ bindings[:object].call_some_method } - end - EOM + raise e.exception <<~ERROR + #{e.message} + If you want to use a RailsAdmin virtual field(= a field without corresponding instance method), + you should declare 'formatted_value' in the field definition. + field :#{name} do + formatted_value{ bindings[:object].call_some_method } + end + ERROR end # Reader for nested attributes @@ -302,12 +352,12 @@ def value end def generic_help - (required? ? I18n.translate('admin.form.required') : I18n.translate('admin.form.optional')) + '. ' + "#{required? ? I18n.translate('admin.form.required') : I18n.translate('admin.form.optional')}. " end def generic_field_help model = abstract_model.model_name.underscore - model_lookup = "admin.help.#{model}.#{name}".to_sym + model_lookup = :"admin.help.#{model}.#{name}" translated = I18n.translate(model_lookup, help: generic_help, default: [generic_help]) (translated.is_a?(Hash) ? translated.to_a.first[1] : translated).html_safe end @@ -317,7 +367,7 @@ def parse_value(value) end def parse_input(_params) - # overriden + # overridden end def inverse_of diff --git a/lib/rails_admin/config/fields/collection_association.rb b/lib/rails_admin/config/fields/collection_association.rb new file mode 100644 index 0000000000..fc08cf264c --- /dev/null +++ b/lib/rails_admin/config/fields/collection_association.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'rails_admin/config/fields/association' + +module RailsAdmin + module Config + module Fields + class CollectionAssociation < Association + # orderable associated objects + register_instance_option :orderable do + false + end + + register_instance_option :partial do + nested_form ? :form_nested_many : :form_filtering_multiselect + end + + def collection(scope = nil) + if scope + super + elsif associated_collection_cache_all + selected = selected_ids + i = 0 + super.sort_by { |a| [selected.index(a[1]) || selected.size, i += 1] } + else + value.map { |o| [o.send(associated_object_label_method), format_key(o.send(associated_primary_key))] } + end + end + + def associated_prepopulate_params + {associated_model_config.abstract_model.param_key => {association.foreign_key => bindings[:object].try(:id)}} + end + + def multiple? + true + end + + def selected_ids + value.map { |s| format_key(s.send(associated_primary_key)).to_s } + end + + def parse_input(params) + return unless associated_model_config.abstract_model.primary_key.is_a?(Array) + + if nested_form + params[method_name].each_value do |value| + value[:id] = associated_model_config.abstract_model.parse_id(value[:id]) + end + elsif params[method_name].is_a?(Array) + params[method_name] = params[method_name].map { |key| associated_model_config.abstract_model.parse_id(key) if key.present? }.compact + if params[method_name].empty? + # Workaround for Arel::Visitors::UnsupportedVisitError in #ids_writer, until https://github.com/rails/rails/pull/51116 is in place + params.delete(method_name) + params[name] = [] + end + end + end + + def form_default_value + (default_value if bindings[:object].new_record? && value.empty?) + end + + def form_value + form_default_value.nil? ? selected_ids : form_default_value + end + + def widget_options + { + xhr: !associated_collection_cache_all, + 'edit-url': (inline_edit && bindings[:view].authorized?(:edit, associated_model_config.abstract_model) ? bindings[:view].edit_path(model_name: associated_model_config.abstract_model.to_param, id: '__ID__') : ''), + remote_source: bindings[:view].index_path(associated_model_config.abstract_model, source_object_id: abstract_model.format_id(bindings[:object].id), source_abstract_model: abstract_model.to_param, associated_collection: name, current_action: bindings[:view].current_action, compact: true), + scopeBy: dynamic_scope_relationships, + sortable: !!orderable, + removable: !!removable, + cacheAll: !!associated_collection_cache_all, + regional: { + add: ::I18n.t('admin.misc.add_new'), + chooseAll: ::I18n.t('admin.misc.chose_all'), + clearAll: ::I18n.t('admin.misc.clear_all'), + down: ::I18n.t('admin.misc.down'), + remove: ::I18n.t('admin.misc.remove'), + search: ::I18n.t('admin.misc.search'), + up: ::I18n.t('admin.misc.up'), + }, + } + end + end + end + end +end diff --git a/lib/rails_admin/config/fields/factories/action_text.rb b/lib/rails_admin/config/fields/factories/action_text.rb index b47dd1563e..2eb95149e4 100644 --- a/lib/rails_admin/config/fields/factories/action_text.rb +++ b/lib/rails_admin/config/fields/factories/action_text.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' diff --git a/lib/rails_admin/config/fields/factories/active_storage.rb b/lib/rails_admin/config/fields/factories/active_storage.rb index 4cc04ddb5a..82f5f1d7b0 100644 --- a/lib/rails_admin/config/fields/factories/active_storage.rb +++ b/lib/rails_admin/config/fields/factories/active_storage.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' require 'rails_admin/config/fields/types/file_upload' @@ -11,12 +13,14 @@ fields << field associations = if properties.type == :has_many - ["#{name}_attachments".to_sym, "#{name}_blobs".to_sym] + [:"#{name}_attachments", :"#{name}_blobs"] else - ["#{name}_attachment".to_sym, "#{name}_blob".to_sym] + [:"#{name}_attachment", :"#{name}_blob"] end children_fields = associations.map do |child_name| - next unless child_association = parent.abstract_model.associations.detect { |p| p.name.to_sym == child_name } + child_association = parent.abstract_model.associations.detect { |p| p.name.to_sym == child_name } + next unless child_association + child_field = fields.detect { |f| f.name == child_name } || RailsAdmin::Config::Fields.default_factory.call(parent, child_association, fields) child_field.hide unless field == child_field child_field.filterable(false) unless field == child_field diff --git a/lib/rails_admin/config/fields/factories/association.rb b/lib/rails_admin/config/fields/factories/association.rb index 46585bafb5..050124db63 100644 --- a/lib/rails_admin/config/fields/factories/association.rb +++ b/lib/rails_admin/config/fields/factories/association.rb @@ -1,25 +1,24 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' require 'rails_admin/config/fields/types/belongs_to_association' RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| - if association = parent.abstract_model.associations.detect { |a| a.foreign_key == properties.name && [:belongs_to, :has_and_belongs_to_many].include?(a.type) } - field = RailsAdmin::Config::Fields::Types.load("#{association.polymorphic? ? :polymorphic : association.type}_association").new(parent, association.name, association) + parent.abstract_model.associations.filter { |a| Array(a.foreign_key).include?(properties.name) && %i[belongs_to has_and_belongs_to_many].include?(a.type) }.each do |association| + field = RailsAdmin::Config::Fields::Types.load(association.field_type).new(parent, association.name, association) fields << field child_columns = [] - possible_field_names = begin - if association.polymorphic? - [:foreign_key, :foreign_type, :foreign_inverse_of] - else - [:foreign_key] - end.collect { |k| association.send(k) }.compact - end + possible_field_names = if association.polymorphic? + %i[foreign_key foreign_type foreign_inverse_of] + else + [:foreign_key] + end.flat_map { |k| Array(association.send(k)) }.compact parent.abstract_model.properties.select { |p| possible_field_names.include? p.name }.each do |column| - unless child_field = fields.detect { |f| f.name.to_s == column.name.to_s } - child_field = RailsAdmin::Config::Fields.default_factory.call(parent, column, fields) - end + child_field = fields.detect { |f| f.name.to_s == column.name.to_s } + child_field ||= RailsAdmin::Config::Fields.default_factory.call(parent, column, fields) child_columns << child_field end @@ -29,5 +28,5 @@ end field.children_fields child_columns.collect(&:name) - end + end.any? end diff --git a/lib/rails_admin/config/fields/factories/carrierwave.rb b/lib/rails_admin/config/fields/factories/carrierwave.rb index 9da7aa6b9d..af1a659f3d 100644 --- a/lib/rails_admin/config/fields/factories/carrierwave.rb +++ b/lib/rails_admin/config/fields/factories/carrierwave.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' require 'rails_admin/config/fields/types/file_upload' @@ -5,14 +7,16 @@ RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| model = parent.abstract_model.model if defined?(::CarrierWave) && model.is_a?(CarrierWave::Mount) && model.uploaders.include?(attachment_name = properties.name.to_s.chomp('_file_name').to_sym) - columns = [model.uploader_options[attachment_name][:mount_on] || attachment_name, "#{attachment_name}_content_type".to_sym, "#{attachment_name}_file_size".to_sym] + columns = [model.uploader_options[attachment_name][:mount_on] || attachment_name, :"#{attachment_name}_content_type", :"#{attachment_name}_file_size"] field = RailsAdmin::Config::Fields::Types.load( - [:serialized, :json].include?(properties.type) ? :multiple_carrierwave : :carrierwave, + %i[serialized json].include?(properties.type) ? :multiple_carrierwave : :carrierwave, ).new(parent, attachment_name, properties) fields << field children_fields = [] columns.each do |children_column_name| - next unless child_properties = parent.abstract_model.properties.detect { |p| p.name.to_s == children_column_name.to_s } + child_properties = parent.abstract_model.properties.detect { |p| p.name.to_s == children_column_name.to_s } + next unless child_properties + children_field = fields.detect { |f| f.name == children_column_name } || RailsAdmin::Config::Fields.default_factory.call(parent, child_properties, fields) children_field.hide unless field == children_field children_field.filterable(false) unless field == children_field diff --git a/lib/rails_admin/config/fields/factories/devise.rb b/lib/rails_admin/config/fields/factories/devise.rb index 6b613afd47..a2f2b9abc9 100644 --- a/lib/rails_admin/config/fields/factories/devise.rb +++ b/lib/rails_admin/config/fields/factories/devise.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' require 'rails_admin/config/fields/types/password' @@ -5,13 +7,15 @@ # Register a custom field factory for devise model RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| if properties.name == :encrypted_password - extensions = [:password_salt, :reset_password_token, :remember_token] + extensions = %i[password_salt reset_password_token remember_token] fields << RailsAdmin::Config::Fields::Types.load(:password).new(parent, :password, properties) fields << RailsAdmin::Config::Fields::Types.load(:password).new(parent, :password_confirmation, properties) extensions.each do |ext| properties = parent.abstract_model.properties.detect { |p| ext == p.name } next unless properties - unless field = fields.detect { |f| f.name == ext } + + field = fields.detect { |f| f.name == ext } + unless field RailsAdmin::Config::Fields.default_factory.call(parent, properties, fields) field = fields.last end diff --git a/lib/rails_admin/config/fields/factories/dragonfly.rb b/lib/rails_admin/config/fields/factories/dragonfly.rb index 578728cb90..b3e400c3ba 100644 --- a/lib/rails_admin/config/fields/factories/dragonfly.rb +++ b/lib/rails_admin/config/fields/factories/dragonfly.rb @@ -1,15 +1,19 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' require 'rails_admin/config/fields/types/file_upload' RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| - extensions = [:name, :uid] + extensions = %i[name uid] if (properties.name.to_s =~ /^(.+)_uid$/) && defined?(::Dragonfly) && parent.abstract_model.model.respond_to?(:dragonfly_attachment_classes) && parent.abstract_model.model.dragonfly_attachment_classes.collect(&:attribute).include?(attachment_name = Regexp.last_match[1].to_sym) field = RailsAdmin::Config::Fields::Types.load(:dragonfly).new(parent, attachment_name, properties) children_fields = [] extensions.each do |ext| - children_column_name = "#{attachment_name}_#{ext}".to_sym - next unless child_properties = parent.abstract_model.properties.detect { |p| p.name.to_s == children_column_name.to_s } + children_column_name = :"#{attachment_name}_#{ext}" + child_properties = parent.abstract_model.properties.detect { |p| p.name.to_s == children_column_name.to_s } + next unless child_properties + children_field = fields.detect { |f| f.name == children_column_name } || RailsAdmin::Config::Fields.default_factory.call(parent, child_properties, fields) children_field.hide children_field.filterable(false) diff --git a/lib/rails_admin/config/fields/factories/enum.rb b/lib/rails_admin/config/fields/factories/enum.rb index a92a2fed39..697d9528fb 100644 --- a/lib/rails_admin/config/fields/factories/enum.rb +++ b/lib/rails_admin/config/fields/factories/enum.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields' require 'rails_admin/config/fields/types/enum' require 'rails_admin/config/fields/types/active_record_enum' diff --git a/lib/rails_admin/config/fields/factories/paperclip.rb b/lib/rails_admin/config/fields/factories/paperclip.rb index 90271f1654..623be41b8c 100644 --- a/lib/rails_admin/config/fields/factories/paperclip.rb +++ b/lib/rails_admin/config/fields/factories/paperclip.rb @@ -1,16 +1,20 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' require 'rails_admin/config/fields/types/file_upload' RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| - extensions = [:file_name, :content_type, :file_size, :updated_at, :fingerprint] + extensions = %i[file_name content_type file_size updated_at fingerprint] model = parent.abstract_model.model if (properties.name.to_s =~ /^(.+)_file_name$/) && defined?(::Paperclip) && model.try(:attachment_definitions) && model.attachment_definitions.key?(attachment_name = Regexp.last_match[1].to_sym) field = RailsAdmin::Config::Fields::Types.load(:paperclip).new(parent, attachment_name, properties) children_fields = [] extensions.each do |ext| - children_column_name = "#{attachment_name}_#{ext}".to_sym - next unless child_properties = parent.abstract_model.properties.detect { |p| p.name.to_s == children_column_name.to_s } + children_column_name = :"#{attachment_name}_#{ext}" + child_properties = parent.abstract_model.properties.detect { |p| p.name.to_s == children_column_name.to_s } + next unless child_properties + children_field = fields.detect { |f| f.name == children_column_name } || RailsAdmin::Config::Fields.default_factory.call(parent, child_properties, fields) children_field.hide children_field.filterable(false) diff --git a/lib/rails_admin/config/fields/factories/password.rb b/lib/rails_admin/config/fields/factories/password.rb index f4fb2314b4..7a97904b72 100644 --- a/lib/rails_admin/config/fields/factories/password.rb +++ b/lib/rails_admin/config/fields/factories/password.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields' require 'rails_admin/config/fields/types/password' diff --git a/lib/rails_admin/config/fields/factories/shrine.rb b/lib/rails_admin/config/fields/factories/shrine.rb index da690c604e..69febad6c3 100644 --- a/lib/rails_admin/config/fields/factories/shrine.rb +++ b/lib/rails_admin/config/fields/factories/shrine.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields' require 'rails_admin/config/fields/types' require 'rails_admin/config/fields/types/file_upload' @@ -5,7 +7,7 @@ RailsAdmin::Config::Fields.register_factory do |parent, properties, fields| next false unless defined?(::Shrine) - attachment_names = parent.abstract_model.model.ancestors.select { |m| m.is_a?(Shrine::Attachment) }.map { |a| a.instance_variable_get("@name") } + attachment_names = parent.abstract_model.model.ancestors.select { |m| m.is_a?(Shrine::Attachment) }.map { |a| a.instance_variable_get('@name') } next false if attachment_names.blank? attachment_name = attachment_names.detect { |a| a == properties.name.to_s.chomp('_data').to_sym } @@ -14,7 +16,7 @@ field = RailsAdmin::Config::Fields::Types.load(:shrine).new(parent, attachment_name, properties) fields << field - data_field_name = "#{attachment_name}_data".to_sym + data_field_name = :"#{attachment_name}_data" child_properties = parent.abstract_model.properties.detect { |p| p.name == data_field_name } next true unless child_properties diff --git a/lib/rails_admin/config/fields/group.rb b/lib/rails_admin/config/fields/group.rb index 9f2be05570..ab41da36dd 100644 --- a/lib/rails_admin/config/fields/group.rb +++ b/lib/rails_admin/config/fields/group.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/core_ext/string/inflections' require 'rails_admin/config/proxyable' require 'rails_admin/config/configurable' @@ -12,9 +14,8 @@ class Group include RailsAdmin::Config::Configurable include RailsAdmin::Config::Hideable - attr_reader :name, :abstract_model + attr_reader :name, :abstract_model, :parent, :root attr_accessor :section - attr_reader :parent, :root def initialize(parent, name) @parent = parent diff --git a/lib/rails_admin/config/fields/singular_association.rb b/lib/rails_admin/config/fields/singular_association.rb new file mode 100644 index 0000000000..44e5654a17 --- /dev/null +++ b/lib/rails_admin/config/fields/singular_association.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'rails_admin/config/fields/association' + +module RailsAdmin + module Config + module Fields + class SingularAssociation < Association + register_instance_option :filter_operators do + %w[_discard like not_like is starts_with ends_with] + (required? ? [] : %w[_separator _present _blank]) + end + + register_instance_option :formatted_value do + (o = value) && o.send(associated_model_config.object_label_method) + end + + register_instance_option :partial do + nested_form ? :form_nested_one : :form_filtering_select + end + + def collection(scope = nil) + if associated_collection_cache_all || scope + super + else + [[formatted_value, selected_id]] + end + end + + def multiple? + false + end + + def selected_id + raise NoMethodError # abstract + end + + def parse_input(params) + return unless nested_form && params[method_name].try(:[], :id).present? + + ids = associated_model_config.abstract_model.parse_id(params[method_name][:id]) + ids = ids.to_composite_keys.to_s if ids.respond_to?(:to_composite_keys) + params[method_name][:id] = ids + end + + def form_value + form_default_value.nil? ? selected_id : form_default_value + end + + def widget_options + { + xhr: !associated_collection_cache_all, + remote_source: bindings[:view].index_path(associated_model_config.abstract_model, source_object_id: abstract_model.format_id(bindings[:object].id), source_abstract_model: abstract_model.to_param, associated_collection: name, current_action: bindings[:view].current_action, compact: true), + scopeBy: dynamic_scope_relationships, + } + end + end + end + end +end diff --git a/lib/rails_admin/config/fields/types.rb b/lib/rails_admin/config/fields/types.rb index 03c3aa8633..f493595eca 100644 --- a/lib/rails_admin/config/fields/types.rb +++ b/lib/rails_admin/config/fields/types.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/core_ext/string/inflections' require 'rails_admin/config/fields' require 'rails_admin/config/fields/association' @@ -9,7 +11,7 @@ module Types @@registry = {} def self.load(type) - @@registry[type.to_sym] || raise("Unsupported field datatype: #{type}") + @@registry.fetch(type.to_sym) { raise "Unsupported field datatype: #{type}" } end def self.register(type, klass = nil) diff --git a/lib/rails_admin/config/fields/types/action_text.rb b/lib/rails_admin/config/fields/types/action_text.rb index 221ac80c32..011c03eeb8 100644 --- a/lib/rails_admin/config/fields/types/action_text.rb +++ b/lib/rails_admin/config/fields/types/action_text.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/text' module RailsAdmin @@ -9,7 +11,7 @@ class ActionText < Text RailsAdmin::Config::Fields::Types.register(self) register_instance_option :version do - '1.1.1' + '1.3.1' end register_instance_option :css_location do @@ -20,6 +22,10 @@ class ActionText < Text "https://cdnjs.cloudflare.com/ajax/libs/trix/#{version}/trix.js" end + register_instance_option :warn_dynamic_load do + true + end + register_instance_option :partial do :form_action_text end diff --git a/lib/rails_admin/config/fields/types/active_record_enum.rb b/lib/rails_admin/config/fields/types/active_record_enum.rb index 640a40d3ed..e0f658a295 100644 --- a/lib/rails_admin/config/fields/types/active_record_enum.rb +++ b/lib/rails_admin/config/fields/types/active_record_enum.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/enum' module RailsAdmin @@ -29,12 +31,14 @@ def type def parse_value(value) return unless value.present? + abstract_model.model.attribute_types[name.to_s].serialize(value) end def parse_input(params) value = params[name] return unless value + params[name] = parse_input_value(value) end diff --git a/lib/rails_admin/config/fields/types/active_storage.rb b/lib/rails_admin/config/fields/types/active_storage.rb index 2b47919e1d..20fe7b472f 100644 --- a/lib/rails_admin/config/fields/types/active_storage.rb +++ b/lib/rails_admin/config/fields/types/active_storage.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/file_upload' module RailsAdmin @@ -8,11 +10,7 @@ class ActiveStorage < RailsAdmin::Config::Fields::Types::FileUpload RailsAdmin::Config::Fields::Types.register(self) register_instance_option :thumb_method do - if ::ActiveStorage::VERSION::MAJOR >= 6 - {resize_to_limit: [100, 100]} - else - {resize: '100x100>'} - end + {resize_to_limit: [100, 100]} end register_instance_option :delete_method do @@ -20,18 +18,41 @@ class ActiveStorage < RailsAdmin::Config::Fields::Types::FileUpload end register_instance_option :image? do - if value - value.filename.to_s.split('.').last =~ /jpg|jpeg|png|gif|svg/i - end + value && (value.representable? || value.content_type.match?(/^image/)) + end + + register_instance_option :eager_load do + {"#{name}_attachment": :blob} + end + + register_instance_option :direct? do + false + end + + register_instance_option :html_attributes do + { + required: required? && !value.present?, + }.merge( + direct? && {data: {direct_upload_url: bindings[:view].main_app.rails_direct_uploads_url}} || {}, + ) + end + + register_instance_option :searchable do + false + end + + register_instance_option :sortable do + false end def resource_url(thumb = false) return nil unless value - if thumb && value.variable? + + if thumb && value.representable? thumb = thumb_method if thumb == true - variant = value.variant(thumb) + representation = value.representation(thumb) Rails.application.routes.url_helpers.rails_blob_representation_path( - variant.blob.signed_id, variant.variation.key, variant.blob.filename, only_path: true + representation.blob.signed_id, representation.variation.key, representation.blob.filename, only_path: true ) else Rails.application.routes.url_helpers.rails_blob_path(value, only_path: true) @@ -40,7 +61,7 @@ def resource_url(thumb = false) def value attachment = super - attachment if attachment && attachment.attached? + attachment if attachment&.attached? end end end diff --git a/lib/rails_admin/config/fields/types/all.rb b/lib/rails_admin/config/fields/types/all.rb index 9ba58c66e8..0d66d3829a 100644 --- a/lib/rails_admin/config/fields/types/all.rb +++ b/lib/rails_admin/config/fields/types/all.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/action_text' require 'rails_admin/config/fields/types/active_record_enum' require 'rails_admin/config/fields/types/active_storage' @@ -38,3 +40,4 @@ require 'rails_admin/config/fields/types/json' require 'rails_admin/config/fields/types/inet' require 'rails_admin/config/fields/types/uuid' +require 'rails_admin/config/fields/types/citext' diff --git a/lib/rails_admin/config/fields/types/belongs_to_association.rb b/lib/rails_admin/config/fields/types/belongs_to_association.rb index ab24df509d..d74034542d 100644 --- a/lib/rails_admin/config/fields/types/belongs_to_association.rb +++ b/lib/rails_admin/config/fields/types/belongs_to_association.rb @@ -1,16 +1,14 @@ -require 'rails_admin/config/fields/association' +# frozen_string_literal: true + +require 'rails_admin/config/fields/singular_association' module RailsAdmin module Config module Fields module Types - class BelongsToAssociation < RailsAdmin::Config::Fields::Association + class BelongsToAssociation < RailsAdmin::Config::Fields::SingularAssociation RailsAdmin::Config::Fields::Types.register(self) - register_instance_option :formatted_value do - (o = value) && o.send(associated_model_config.object_label_method) - end - register_instance_option :sortable do @sortable ||= abstract_model.adapter_supports_joins? && associated_model_config.abstract_model.properties.collect(&:name).include?(associated_model_config.object_label_method) ? associated_model_config.object_label_method : {abstract_model.table_name => method_name} end @@ -19,36 +17,29 @@ class BelongsToAssociation < RailsAdmin::Config::Fields::Association @searchable ||= associated_model_config.abstract_model.properties.collect(&:name).include?(associated_model_config.object_label_method) ? [associated_model_config.object_label_method, {abstract_model.model => method_name}] : {abstract_model.model => method_name} end - register_instance_option :partial do - nested_form ? :form_nested_one : :form_filtering_select - end - - register_instance_option :inline_add do - true - end - - register_instance_option :inline_edit do - true - end - - register_instance_option :eager_load? do + register_instance_option :eager_load do true end - def associated_primary_key - @associated_primary_key ||= association.primary_key + register_instance_option :allowed_methods do + nested_form ? [method_name] : Array(association.foreign_key) end def selected_id - bindings[:object].send(foreign_key) + if association.foreign_key.is_a?(Array) + format_key(association.foreign_key.map { |attribute| bindings[:object].safe_send(attribute) }) + else + bindings[:object].safe_send(association.key_accessor) + end end - def method_name - nested_form ? "#{name}_attributes".to_sym : association.foreign_key - end + def parse_input(params) + return super if nested_form + return unless params[method_name].present? && association.foreign_key.is_a?(Array) - def multiple? - false + association.foreign_key.zip(RailsAdmin.config.composite_keys_serializer.deserialize(params.delete(method_name))).each do |key, value| + params[key] = value + end end end end diff --git a/lib/rails_admin/config/fields/types/boolean.rb b/lib/rails_admin/config/fields/types/boolean.rb index 865a939f42..7c2839702f 100644 --- a/lib/rails_admin/config/fields/types/boolean.rb +++ b/lib/rails_admin/config/fields/types/boolean.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Fields @@ -6,19 +8,36 @@ class Boolean < RailsAdmin::Config::Fields::Base # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) + register_instance_option :labels do + { + true => %(), + false => %(), + nil => %(), + } + end + + register_instance_option :css_classes do + { + true => 'success', + false => 'danger', + nil => 'default', + } + end + + register_instance_option :filter_operators do + %w[_discard true false] + (required? ? [] : %w[_separator _present _blank]) + end + + register_instance_option :nullable? do + properties&.nullable? + end + register_instance_option :view_helper do :check_box end register_instance_option :pretty_value do - case value - when nil - %() - when false - %() - when true - %() - end.html_safe + %(#{labels[form_value]}).html_safe end register_instance_option :export_value do @@ -29,10 +48,21 @@ class Boolean < RailsAdmin::Config::Fields::Base :form_boolean end + def form_value + case value + when true, false + value + end + end + # Accessor for field's help text displayed below input field. def generic_help '' end + + def parse_input(params) + params[name] = params[name].presence if params.key?(name) + end end end end diff --git a/lib/rails_admin/config/fields/types/bson_object_id.rb b/lib/rails_admin/config/fields/types/bson_object_id.rb index 59f3077ee0..8bc972e809 100644 --- a/lib/rails_admin/config/fields/types/bson_object_id.rb +++ b/lib/rails_admin/config/fields/types/bson_object_id.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/string' module RailsAdmin diff --git a/lib/rails_admin/config/fields/types/carrierwave.rb b/lib/rails_admin/config/fields/types/carrierwave.rb index 31dd5b8cc0..7fd6f5a974 100644 --- a/lib/rails_admin/config/fields/types/carrierwave.rb +++ b/lib/rails_admin/config/fields/types/carrierwave.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/base' require 'rails_admin/config/fields/types/file_upload' @@ -22,6 +24,7 @@ class Carrierwave < RailsAdmin::Config::Fields::Types::FileUpload def resource_url(thumb = false) return nil unless (uploader = bindings[:object].send(name)).present? + thumb.present? ? uploader.send(thumb).url : uploader.url end end diff --git a/lib/rails_admin/config/fields/types/citext.rb b/lib/rails_admin/config/fields/types/citext.rb new file mode 100644 index 0000000000..c93c0b8af7 --- /dev/null +++ b/lib/rails_admin/config/fields/types/citext.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'rails_admin/config/fields/types/text' + +module RailsAdmin + module Config + module Fields + module Types + class Citext < Text + RailsAdmin::Config::Fields::Types.register(:citext, self) + end + end + end + end +end diff --git a/lib/rails_admin/config/fields/types/ck_editor.rb b/lib/rails_admin/config/fields/types/ck_editor.rb index f1f4f439bd..dd6ed249f7 100644 --- a/lib/rails_admin/config/fields/types/ck_editor.rb +++ b/lib/rails_admin/config/fields/types/ck_editor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/text' module RailsAdmin diff --git a/lib/rails_admin/config/fields/types/code_mirror.rb b/lib/rails_admin/config/fields/types/code_mirror.rb index 0d45a16ae7..fbd08f3eee 100644 --- a/lib/rails_admin/config/fields/types/code_mirror.rb +++ b/lib/rails_admin/config/fields/types/code_mirror.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/text' module RailsAdmin diff --git a/lib/rails_admin/config/fields/types/color.rb b/lib/rails_admin/config/fields/types/color.rb index 554d2acbcc..a18bc5ea7e 100644 --- a/lib/rails_admin/config/fields/types/color.rb +++ b/lib/rails_admin/config/fields/types/color.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/string_like' module RailsAdmin @@ -15,10 +17,14 @@ class Color < StringLike :form_colorpicker end + register_instance_option :view_helper do + :color_field + end + register_instance_option :color do if value.present? - if value =~ /^[0-9a-fA-F]{3,6}$/ - '#' + value + if /^[0-9a-fA-F]{3,6}$/.match?(value) + "##{value}" else value end diff --git a/lib/rails_admin/config/fields/types/date.rb b/lib/rails_admin/config/fields/types/date.rb index 825be17f5f..02230d635a 100644 --- a/lib/rails_admin/config/fields/types/date.rb +++ b/lib/rails_admin/config/fields/types/date.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/datetime' module RailsAdmin @@ -7,21 +9,25 @@ module Types class Date < RailsAdmin::Config::Fields::Types::Datetime RailsAdmin::Config::Fields::Types.register(self) - register_instance_option :date_format do - :long + def parse_value(value) + ::Date.parse(value) if value.present? end - register_instance_option :i18n_scope do - [:date, :formats] + register_instance_option :date_format do + :long end register_instance_option :datepicker_options do { - showTodayButton: true, - format: parser.to_momentjs, + allowInput: true, + altFormat: flatpickr_format, } end + register_instance_option :i18n_scope do + %i[date formats] + end + register_instance_option :html_attributes do { required: required?, diff --git a/lib/rails_admin/config/fields/types/datetime.rb b/lib/rails_admin/config/fields/types/datetime.rb index 8602697ddd..8013b724c9 100644 --- a/lib/rails_admin/config/fields/types/datetime.rb +++ b/lib/rails_admin/config/fields/types/datetime.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/base' require 'rails_admin/support/datetime' @@ -8,25 +10,22 @@ module Types class Datetime < RailsAdmin::Config::Fields::Base RailsAdmin::Config::Fields::Types.register(self) - def parser - RailsAdmin::Support::Datetime.new(strftime_format) - end - def parse_value(value) - parser.parse_string(value) + ::Time.zone.parse(value) end def parse_input(params) params[name] = parse_value(params[name]) if params[name] end - def value - parent_value = super - if %w(DateTime Date Time).include?(parent_value.class.name) - parent_value.in_time_zone - else - parent_value - end + register_instance_option :filter_operators do + %w[default between today yesterday this_week last_week] + (required? ? [] : %w[_separator _not_null _null]) + end + + def filter_options + super.merge( + datetimepicker_options: datepicker_options, + ) end register_instance_option :date_format do @@ -34,21 +33,24 @@ def value end register_instance_option :i18n_scope do - [:time, :formats] + %i[time formats] end register_instance_option :strftime_format do - begin - ::I18n.t(date_format, scope: i18n_scope, raise: true) - rescue ::I18n::ArgumentError - "%B %d, %Y %H:%M" - end + ::I18n.t(date_format, scope: i18n_scope, raise: true) + rescue ::I18n::ArgumentError + '%B %d, %Y %H:%M' + end + + register_instance_option :flatpickr_format do + RailsAdmin::Support::Datetime.to_flatpickr_format(strftime_format) end register_instance_option :datepicker_options do { - showTodayButton: true, - format: parser.to_momentjs, + allowInput: true, + enableTime: true, + altFormat: flatpickr_format, } end @@ -63,8 +65,13 @@ def value true end + register_instance_option :queryable? do + false + end + register_instance_option :formatted_value do - if time = (value || default_value) + time = (value || default_value) + if time ::I18n.l(time, format: strftime_format) else ''.html_safe @@ -74,6 +81,14 @@ def value register_instance_option :partial do :form_datetime end + + register_deprecated_instance_option :momentjs_format do + ActiveSupport::Deprecation.warn('The momentjs_format configuration option is deprecated, please use flatpickr_format with corresponding values here: https://flatpickr.js.org/formatting/') + end + + def form_value + value&.in_time_zone&.strftime('%FT%T') || form_default_value + end end end end diff --git a/lib/rails_admin/config/fields/types/decimal.rb b/lib/rails_admin/config/fields/types/decimal.rb index 9be21c9c2f..166564f74d 100644 --- a/lib/rails_admin/config/fields/types/decimal.rb +++ b/lib/rails_admin/config/fields/types/decimal.rb @@ -1,12 +1,21 @@ -require 'rails_admin/config/fields/base' +# frozen_string_literal: true + +require 'rails_admin/config/fields/types/numeric' module RailsAdmin module Config module Fields module Types - class Decimal < RailsAdmin::Config::Fields::Base + class Decimal < RailsAdmin::Config::Fields::Types::Numeric # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) + + register_instance_option :html_attributes do + { + required: required?, + step: 'any', + } + end end end end diff --git a/lib/rails_admin/config/fields/types/dragonfly.rb b/lib/rails_admin/config/fields/types/dragonfly.rb index e324b40dcf..80f9fe5a4e 100644 --- a/lib/rails_admin/config/fields/types/dragonfly.rb +++ b/lib/rails_admin/config/fields/types/dragonfly.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/base' require 'rails_admin/config/fields/types/file_upload' @@ -10,9 +12,9 @@ class Dragonfly < RailsAdmin::Config::Fields::Types::FileUpload RailsAdmin::Config::Fields::Types.register(self) register_instance_option :image? do - false unless value if abstract_model.model.new.respond_to?("#{name}_name") - bindings[:object].send("#{name}_name").to_s.split('.').last =~ /jpg|jpeg|png|gif/i + mime_type = Mime::Type.lookup_by_extension(bindings[:object].send("#{name}_name").to_s.split('.').last) + mime_type.to_s.match?(/^image/) else true # Dragonfly really is image oriented end @@ -32,6 +34,7 @@ class Dragonfly < RailsAdmin::Config::Fields::Types::FileUpload def resource_url(thumb = false) return nil unless (v = value) + thumb ? v.thumb(thumb).try(:url) : v.url end end diff --git a/lib/rails_admin/config/fields/types/enum.rb b/lib/rails_admin/config/fields/types/enum.rb index b60cf99128..961e6ff489 100644 --- a/lib/rails_admin/config/fields/types/enum.rb +++ b/lib/rails_admin/config/fields/types/enum.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/string' module RailsAdmin @@ -7,21 +9,32 @@ module Types class Enum < RailsAdmin::Config::Fields::Base RailsAdmin::Config::Fields::Types.register(self) + register_instance_option :filter_operators do + %w[_discard] + + enum.map do |label, value| + {label: label, value: value || label} + end + (required? ? [] : %w[_separator _present _blank]) + end + register_instance_option :partial do :form_enumeration end register_instance_option :enum_method do - @enum_method ||= bindings[:object].class.respond_to?("#{name}_enum") || bindings[:object].respond_to?("#{name}_enum") ? "#{name}_enum" : name + @enum_method ||= bindings[:object].class.respond_to?("#{name}_enum") || (bindings[:object] || abstract_model.model.new).respond_to?("#{name}_enum") ? "#{name}_enum" : name end register_instance_option :enum do - bindings[:object].class.respond_to?(enum_method) ? bindings[:object].class.send(enum_method) : bindings[:object].send(enum_method) + if abstract_model.model.respond_to?(enum_method) + abstract_model.model.send(enum_method) + else + (bindings[:object] || abstract_model.model.new).send(enum_method) + end end register_instance_option :pretty_value do if enum.is_a?(::Hash) - enum.reject { |_k, v| v.to_s != value.to_s }.keys.first.to_s.presence || value.presence || ' - ' + enum.select { |_k, v| v.to_s == value.to_s }.keys.first.to_s.presence || value.presence || ' - ' elsif enum.is_a?(::Array) && enum.first.is_a?(::Array) enum.detect { |e| e[1].to_s == value.to_s }.try(:first).to_s.presence || value.presence || ' - ' else diff --git a/lib/rails_admin/config/fields/types/file_upload.rb b/lib/rails_admin/config/fields/types/file_upload.rb index e49531ce8f..c3f96fa4e9 100644 --- a/lib/rails_admin/config/fields/types/file_upload.rb +++ b/lib/rails_admin/config/fields/types/file_upload.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/string' module RailsAdmin @@ -42,7 +44,7 @@ class FileUpload < RailsAdmin::Config::Fields::Base if image thumb_url = resource_url(thumb_method) image_html = v.image_tag(thumb_url, class: 'img-thumbnail') - url != thumb_url ? v.link_to(image_html, url, target: '_blank', rel: 'noopener noreferrer') : image_html + url == thumb_url ? image_html : v.link_to(image_html, url, target: '_blank', rel: 'noopener noreferrer') else v.link_to(link_name, url, target: '_blank', rel: 'noopener noreferrer') end @@ -50,7 +52,8 @@ class FileUpload < RailsAdmin::Config::Fields::Base end register_instance_option :image? do - (url = resource_url.to_s) && url.split('.').last =~ /jpg|jpeg|png|gif|svg/i + mime_type = Mime::Type.lookup_by_extension(extension) + mime_type.to_s.match?(/^image/) end register_instance_option :allowed_methods do @@ -63,9 +66,15 @@ class FileUpload < RailsAdmin::Config::Fields::Base } end + def extension + URI.parse(resource_url).path.split('.').last + rescue URI::InvalidURIError + nil + end + # virtual class def resource_url - raise('not implemented') + raise 'not implemented' end def virtual? diff --git a/lib/rails_admin/config/fields/types/float.rb b/lib/rails_admin/config/fields/types/float.rb index 6a0937e249..c68c7aef8c 100644 --- a/lib/rails_admin/config/fields/types/float.rb +++ b/lib/rails_admin/config/fields/types/float.rb @@ -1,12 +1,21 @@ -require 'rails_admin/config/fields/base' +# frozen_string_literal: true + +require 'rails_admin/config/fields/types/numeric' module RailsAdmin module Config module Fields module Types - class Float < RailsAdmin::Config::Fields::Base + class Float < RailsAdmin::Config::Fields::Types::Numeric # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) + + register_instance_option :html_attributes do + { + required: required?, + step: 'any', + } + end end end end diff --git a/lib/rails_admin/config/fields/types/froala.rb b/lib/rails_admin/config/fields/types/froala.rb index a88cc78013..b52095d493 100644 --- a/lib/rails_admin/config/fields/types/froala.rb +++ b/lib/rails_admin/config/fields/types/froala.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/text' module RailsAdmin diff --git a/lib/rails_admin/config/fields/types/has_and_belongs_to_many_association.rb b/lib/rails_admin/config/fields/types/has_and_belongs_to_many_association.rb index 656ce8b6d7..1f63c2cf75 100644 --- a/lib/rails_admin/config/fields/types/has_and_belongs_to_many_association.rb +++ b/lib/rails_admin/config/fields/types/has_and_belongs_to_many_association.rb @@ -1,10 +1,12 @@ -require 'rails_admin/config/fields/types/has_many_association' +# frozen_string_literal: true + +require 'rails_admin/config/fields/collection_association' module RailsAdmin module Config module Fields module Types - class HasAndBelongsToManyAssociation < RailsAdmin::Config::Fields::Types::HasManyAssociation + class HasAndBelongsToManyAssociation < RailsAdmin::Config::Fields::CollectionAssociation # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) end diff --git a/lib/rails_admin/config/fields/types/has_many_association.rb b/lib/rails_admin/config/fields/types/has_many_association.rb index c89386f86b..dd5609d100 100644 --- a/lib/rails_admin/config/fields/types/has_many_association.rb +++ b/lib/rails_admin/config/fields/types/has_many_association.rb @@ -1,34 +1,14 @@ -require 'rails_admin/config/fields/association' +# frozen_string_literal: true + +require 'rails_admin/config/fields/collection_association' module RailsAdmin module Config module Fields module Types - class HasManyAssociation < RailsAdmin::Config::Fields::Association + class HasManyAssociation < RailsAdmin::Config::Fields::CollectionAssociation # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) - - register_instance_option :partial do - nested_form ? :form_nested_many : :form_filtering_multiselect - end - - # orderable associated objects - register_instance_option :orderable do - false - end - - register_instance_option :inline_add do - true - end - - def method_name - nested_form ? "#{super}_attributes".to_sym : "#{super.to_s.singularize}_ids".to_sym # name_ids - end - - # Reader for validation errors of the bound object - def errors - bindings[:object].errors[name] - end end end end diff --git a/lib/rails_admin/config/fields/types/has_one_association.rb b/lib/rails_admin/config/fields/types/has_one_association.rb index c330876381..ba1b48999b 100644 --- a/lib/rails_admin/config/fields/types/has_one_association.rb +++ b/lib/rails_admin/config/fields/types/has_one_association.rb @@ -1,44 +1,32 @@ -require 'rails_admin/config/fields/association' +# frozen_string_literal: true + +require 'rails_admin/config/fields/singular_association' module RailsAdmin module Config module Fields module Types - class HasOneAssociation < RailsAdmin::Config::Fields::Association + class HasOneAssociation < RailsAdmin::Config::Fields::SingularAssociation # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) - register_instance_option :partial do - nested_form ? :form_nested_one : :form_filtering_select - end - - # Accessor for field's formatted value - register_instance_option :formatted_value do - (o = value) && o.send(associated_model_config.object_label_method) + register_instance_option :allowed_methods do + nested_form ? [method_name] : [name] end - register_instance_option :inline_add do - true + def associated_prepopulate_params + {associated_model_config.abstract_model.param_key => {association.foreign_key => bindings[:object].try(:id)}} end - register_instance_option :inline_edit do - true - end + def parse_input(params) + return super if nested_form - def editable? - (nested_form || abstract_model.model.new.respond_to?("#{name}_id=")) && super + id = params.delete(method_name) + params[name] = associated_model_config.abstract_model.get(id) if id end def selected_id - value.try :id - end - - def method_name - nested_form ? "#{name}_attributes".to_sym : "#{name}_id".to_sym - end - - def multiple? - false + format_key(value.try(:id)).try(:to_s) end end end diff --git a/lib/rails_admin/config/fields/types/hidden.rb b/lib/rails_admin/config/fields/types/hidden.rb index 568ec96828..0930a79711 100644 --- a/lib/rails_admin/config/fields/types/hidden.rb +++ b/lib/rails_admin/config/fields/types/hidden.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/string_like' module RailsAdmin diff --git a/lib/rails_admin/config/fields/types/inet.rb b/lib/rails_admin/config/fields/types/inet.rb index 614af909ec..bcddcf6a36 100644 --- a/lib/rails_admin/config/fields/types/inet.rb +++ b/lib/rails_admin/config/fields/types/inet.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/base' module RailsAdmin diff --git a/lib/rails_admin/config/fields/types/integer.rb b/lib/rails_admin/config/fields/types/integer.rb index 08e180b025..4dd14e500d 100644 --- a/lib/rails_admin/config/fields/types/integer.rb +++ b/lib/rails_admin/config/fields/types/integer.rb @@ -1,17 +1,15 @@ -require 'rails_admin/config/fields/base' +# frozen_string_literal: true + +require 'rails_admin/config/fields/types/numeric' module RailsAdmin module Config module Fields module Types - class Integer < RailsAdmin::Config::Fields::Base + class Integer < RailsAdmin::Config::Fields::Types::Numeric # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) - register_instance_option :view_helper do - :number_field - end - register_instance_option :sort_reverse? do serial? end diff --git a/lib/rails_admin/config/fields/types/json.rb b/lib/rails_admin/config/fields/types/json.rb index 366c919fe7..024f0c41bc 100644 --- a/lib/rails_admin/config/fields/types/json.rb +++ b/lib/rails_admin/config/fields/types/json.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/text' module RailsAdmin diff --git a/lib/rails_admin/config/fields/types/multiple_active_storage.rb b/lib/rails_admin/config/fields/types/multiple_active_storage.rb index 7c95eb18c0..43d163e029 100644 --- a/lib/rails_admin/config/fields/types/multiple_active_storage.rb +++ b/lib/rails_admin/config/fields/types/multiple_active_storage.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/multiple_file_upload' module RailsAdmin @@ -9,11 +11,11 @@ class MultipleActiveStorage < RailsAdmin::Config::Fields::Types::MultipleFileUpl class ActiveStorageAttachment < RailsAdmin::Config::Fields::Types::MultipleFileUpload::AbstractAttachment register_instance_option :thumb_method do - if ::ActiveStorage::VERSION::MAJOR >= 6 - {resize_to_limit: [100, 100]} - else - {resize: '100x100>'} - end + {resize_to_limit: [100, 100]} + end + + register_instance_option :keep_value do + value.signed_id end register_instance_option :delete_value do @@ -21,17 +23,16 @@ class ActiveStorageAttachment < RailsAdmin::Config::Fields::Types::MultipleFileU end register_instance_option :image? do - if value - value.filename.to_s.split('.').last =~ /jpg|jpeg|png|gif|svg/i - end + value && (value.representable? || value.content_type.match?(/^image/)) end def resource_url(thumb = false) return nil unless value - if thumb && value.variable? - variant = value.variant(thumb_method) + + if thumb && value.representable? + representation = value.representation(thumb_method) Rails.application.routes.url_helpers.rails_blob_representation_path( - variant.blob.signed_id, variant.variation.key, variant.blob.filename, only_path: true + representation.blob.signed_id, representation.variation.key, representation.blob.filename, only_path: true ) else Rails.application.routes.url_helpers.rails_blob_path(value, only_path: true) @@ -43,9 +44,37 @@ def resource_url(thumb = false) ActiveStorageAttachment end + register_instance_option :keep_method do + method_name if ::ActiveStorage.gem_version >= Gem::Version.new('7.1') || ::ActiveStorage.replace_on_assign_to_many + end + register_instance_option :delete_method do "remove_#{name}" if bindings[:object].respond_to?("remove_#{name}") end + + register_instance_option :eager_load do + {"#{name}_attachments": :blob} + end + + register_instance_option :direct? do + false + end + + register_instance_option :html_attributes do + { + required: required? && !value.present?, + }.merge( + direct? && {data: {direct_upload_url: bindings[:view].main_app.rails_direct_uploads_url}} || {}, + ) + end + + register_instance_option :searchable do + false + end + + register_instance_option :sortable do + false + end end end end diff --git a/lib/rails_admin/config/fields/types/multiple_carrierwave.rb b/lib/rails_admin/config/fields/types/multiple_carrierwave.rb index 7366fec259..0a15b5637e 100644 --- a/lib/rails_admin/config/fields/types/multiple_carrierwave.rb +++ b/lib/rails_admin/config/fields/types/multiple_carrierwave.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/multiple_file_upload' module RailsAdmin @@ -22,6 +24,7 @@ class CarrierwaveAttachment < RailsAdmin::Config::Fields::Types::MultipleFileUpl def resource_url(thumb = false) return nil unless value + thumb.present? ? value.send(thumb).url : value.url end end diff --git a/lib/rails_admin/config/fields/types/multiple_file_upload.rb b/lib/rails_admin/config/fields/types/multiple_file_upload.rb index 47c361489e..05f6afaa9f 100644 --- a/lib/rails_admin/config/fields/types/multiple_file_upload.rb +++ b/lib/rails_admin/config/fields/types/multiple_file_upload.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Fields @@ -36,7 +38,7 @@ def initialize(value) if image thumb_url = resource_url(thumb_method) image_html = v.image_tag(thumb_url, class: 'img-thumbnail') - url != thumb_url ? v.link_to(image_html, url, target: '_blank', rel: 'noopener noreferrer') : image_html + url == thumb_url ? image_html : v.link_to(image_html, url, target: '_blank', rel: 'noopener noreferrer') else display_value = value.respond_to?(:filename) ? value.filename : value v.link_to(display_value, url, target: '_blank', rel: 'noopener noreferrer') @@ -45,11 +47,18 @@ def initialize(value) end register_instance_option :image? do - (url = resource_url.to_s) && url.split('.').last =~ /jpg|jpeg|png|gif|svg/i + mime_type = Mime::Type.lookup_by_extension(extension) + mime_type.to_s.match?(/^image/) end def resource_url(_thumb = false) - raise('not implemented') + raise 'not implemented' + end + + def extension + URI.parse(resource_url).path.split('.').last + rescue URI::InvalidURIError + nil end end diff --git a/lib/rails_admin/config/fields/types/numeric.rb b/lib/rails_admin/config/fields/types/numeric.rb new file mode 100644 index 0000000000..f6c086cf62 --- /dev/null +++ b/lib/rails_admin/config/fields/types/numeric.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_admin/config/fields/base' + +module RailsAdmin + module Config + module Fields + module Types + class Numeric < RailsAdmin::Config::Fields::Base + # Register field type for the type loader + RailsAdmin::Config::Fields::Types.register(self) + + register_instance_option :filter_operators do + %w[default between] + (required? ? [] : %w[_separator _not_null _null]) + end + + register_instance_option :view_helper do + :number_field + end + end + end + end + end +end diff --git a/lib/rails_admin/config/fields/types/paperclip.rb b/lib/rails_admin/config/fields/types/paperclip.rb index 23fad142af..399b334efd 100644 --- a/lib/rails_admin/config/fields/types/paperclip.rb +++ b/lib/rails_admin/config/fields/types/paperclip.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/base' require 'rails_admin/config/fields/types/file_upload' diff --git a/lib/rails_admin/config/fields/types/password.rb b/lib/rails_admin/config/fields/types/password.rb index b7cd0c9422..9cac5cc897 100644 --- a/lib/rails_admin/config/fields/types/password.rb +++ b/lib/rails_admin/config/fields/types/password.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/string' module RailsAdmin diff --git a/lib/rails_admin/config/fields/types/polymorphic_association.rb b/lib/rails_admin/config/fields/types/polymorphic_association.rb index 6d3187878d..7632df92f9 100644 --- a/lib/rails_admin/config/fields/types/polymorphic_association.rb +++ b/lib/rails_admin/config/fields/types/polymorphic_association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/belongs_to_association' module RailsAdmin @@ -45,29 +47,33 @@ class PolymorphicAssociation < RailsAdmin::Config::Fields::Types::BelongsToAssoc [children_fields] end - register_instance_option :eager_load? do + register_instance_option :eager_load do false end - def associated_collection(type) - return [] if type.blank? - config = RailsAdmin.config(type) - config.abstract_model.all.collect do |object| - [object.send(config.object_label_method), object.id] + def associated_model_config + @associated_model_config ||= association.klass.collect { |type| RailsAdmin.config(type) }.reject(&:excluded?) + end + + def collection(_scope = nil) + if value + [[formatted_value, selected_id]] + else + [[]] end end - def associated_model_config - @associated_model_config ||= association.klass.collect { |type| RailsAdmin.config(type) }.select { |config| !config.excluded? } + def type_column + association.foreign_type.to_s end - def polymorphic_type_collection + def type_collection associated_model_config.collect do |config| [config.label, config.abstract_model.model.name] end end - def polymorphic_type_urls + def type_urls types = associated_model_config.collect do |config| [config.abstract_model.model.name, config.abstract_model.to_param] end @@ -79,6 +85,26 @@ def value bindings[:object].send(association.name) end + def widget_options_for_types + type_collection.inject({}) do |options, model| + options.merge( + model.second.downcase.gsub('::', '-') => { + xhr: true, + remote_source: bindings[:view].index_path(model.second.underscore, source_object_id: bindings[:object].id, source_abstract_model: abstract_model.to_param, current_action: bindings[:view].current_action, compact: true), + float_left: false, + }, + ) + end + end + + def widget_options + widget_options_for_types[selected_type.try(:downcase)] || {float_left: false} + end + + def selected_type + bindings[:object].send(type_column) + end + def parse_input(params) if (type_value = params[association.foreign_type.to_sym]).present? config = associated_model_config.find { |c| type_value == c.abstract_model.model.name } diff --git a/lib/rails_admin/config/fields/types/serialized.rb b/lib/rails_admin/config/fields/types/serialized.rb index 491bead259..d403ee56eb 100644 --- a/lib/rails_admin/config/fields/types/serialized.rb +++ b/lib/rails_admin/config/fields/types/serialized.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/text' module RailsAdmin diff --git a/lib/rails_admin/config/fields/types/shrine.rb b/lib/rails_admin/config/fields/types/shrine.rb index 3ed8978556..bdac240a40 100644 --- a/lib/rails_admin/config/fields/types/shrine.rb +++ b/lib/rails_admin/config/fields/types/shrine.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/file_upload' module RailsAdmin diff --git a/lib/rails_admin/config/fields/types/simple_mde.rb b/lib/rails_admin/config/fields/types/simple_mde.rb index ffd72949c0..6d0cda18c4 100644 --- a/lib/rails_admin/config/fields/types/simple_mde.rb +++ b/lib/rails_admin/config/fields/types/simple_mde.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/text' module RailsAdmin diff --git a/lib/rails_admin/config/fields/types/string.rb b/lib/rails_admin/config/fields/types/string.rb index e0a322f868..19403a1c9c 100644 --- a/lib/rails_admin/config/fields/types/string.rb +++ b/lib/rails_admin/config/fields/types/string.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/string_like' module RailsAdmin @@ -20,7 +22,7 @@ def input_size end def generic_help - text = (required? ? I18n.translate('admin.form.required') : I18n.translate('admin.form.optional')) + '. ' + text = "#{required? ? I18n.translate('admin.form.required') : I18n.translate('admin.form.optional')}. " if valid_length.present? && valid_length[:is].present? text += "#{I18n.translate('admin.form.char_length_of').capitalize} #{valid_length[:is]}." else diff --git a/lib/rails_admin/config/fields/types/string_like.rb b/lib/rails_admin/config/fields/types/string_like.rb index fcbf564844..5f3e9507e2 100644 --- a/lib/rails_admin/config/fields/types/string_like.rb +++ b/lib/rails_admin/config/fields/types/string_like.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/base' module RailsAdmin @@ -5,6 +7,10 @@ module Config module Fields module Types class StringLike < RailsAdmin::Config::Fields::Base + register_instance_option :filter_operators do + %w[_discard like not_like is starts_with ends_with] + (required? ? [] : %w[_separator _present _blank]) + end + register_instance_option :treat_empty_as_nil? do properties.try(:nullable?) end diff --git a/lib/rails_admin/config/fields/types/text.rb b/lib/rails_admin/config/fields/types/text.rb index 3fb6ff2099..d51c767240 100644 --- a/lib/rails_admin/config/fields/types/text.rb +++ b/lib/rails_admin/config/fields/types/text.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/string_like' module RailsAdmin diff --git a/lib/rails_admin/config/fields/types/time.rb b/lib/rails_admin/config/fields/types/time.rb index cc9a609bd6..4294cc92e3 100644 --- a/lib/rails_admin/config/fields/types/time.rb +++ b/lib/rails_admin/config/fields/types/time.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/datetime' module RailsAdmin @@ -8,10 +10,20 @@ class Time < RailsAdmin::Config::Fields::Types::Datetime RailsAdmin::Config::Fields::Types.register(self) def parse_value(value) - parent_value = super(value) - return unless parent_value - value_with_tz = parent_value.in_time_zone - ::DateTime.parse(value_with_tz.strftime('%Y-%m-%d %H:%M:%S')) + abstract_model.model.type_for_attribute(name.to_s).serialize(super)&.change(year: 2000, month: 1, day: 1) + end + + register_instance_option :filter_operators do + %w[default between] + (required? ? [] : %w[_separator _not_null _null]) + end + + register_instance_option :datepicker_options do + { + allowInput: true, + altFormat: flatpickr_format, + enableTime: true, + noCalendar: true, + } end register_instance_option :strftime_format do diff --git a/lib/rails_admin/config/fields/types/timestamp.rb b/lib/rails_admin/config/fields/types/timestamp.rb index 8025e7ff7a..f677bc76c3 100644 --- a/lib/rails_admin/config/fields/types/timestamp.rb +++ b/lib/rails_admin/config/fields/types/timestamp.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/datetime' module RailsAdmin @@ -7,10 +9,6 @@ module Types class Timestamp < RailsAdmin::Config::Fields::Types::Datetime # Register field type for the type loader RailsAdmin::Config::Fields::Types.register(self) - - @format = :long - @i18n_scope = [:time, :formats] - @js_plugin_options = {} end end end diff --git a/lib/rails_admin/config/fields/types/uuid.rb b/lib/rails_admin/config/fields/types/uuid.rb index b0eea09433..333589048c 100644 --- a/lib/rails_admin/config/fields/types/uuid.rb +++ b/lib/rails_admin/config/fields/types/uuid.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/string' module RailsAdmin diff --git a/lib/rails_admin/config/fields/types/wysihtml5.rb b/lib/rails_admin/config/fields/types/wysihtml5.rb index f7a3b7ea88..ce39829723 100644 --- a/lib/rails_admin/config/fields/types/wysihtml5.rb +++ b/lib/rails_admin/config/fields/types/wysihtml5.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/types/text' module RailsAdmin diff --git a/lib/rails_admin/config/groupable.rb b/lib/rails_admin/config/groupable.rb index 7e78786ef4..096e176a83 100644 --- a/lib/rails_admin/config/groupable.rb +++ b/lib/rails_admin/config/groupable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/group' module RailsAdmin diff --git a/lib/rails_admin/config/has_description.rb b/lib/rails_admin/config/has_description.rb index 26bebf9627..299728a5c0 100644 --- a/lib/rails_admin/config/has_description.rb +++ b/lib/rails_admin/config/has_description.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config # Provides accessor and autoregistering of model's description. diff --git a/lib/rails_admin/config/has_fields.rb b/lib/rails_admin/config/has_fields.rb index baee2685fc..864b9a428b 100644 --- a/lib/rails_admin/config/has_fields.rb +++ b/lib/rails_admin/config/has_fields.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config # Provides accessors and autoregistering of model's fields. @@ -8,9 +10,7 @@ def field(name, type = nil, add_to_section = true, &block) # some fields are hidden by default (belongs_to keys, has_many associations in list views.) # unhide them if config specifically defines them - if field - field.show unless field.instance_variable_get("@#{field.name}_registered").is_a?(Proc) - end + field.show if field && !field.instance_variable_get("@#{field.name}_registered").is_a?(Proc) # Specify field as virtual if type is not specifically set and field was not # found in default stack if field.nil? && type.nil? @@ -39,17 +39,18 @@ def field(name, type = nil, add_to_section = true, &block) field end - # configure a field without adding it. + # configure field(s) from the default group in a section without changing the original order. def configure(name, type = nil, &block) - field(name, type, false, &block) + [*name].each { |field_name| field(field_name, type, false, &block) } end - # include fields by name and apply an optionnal block to each (through a call to fields), + # include fields by name and apply an optional block to each (through a call to fields), # or include fields by conditions if no field names def include_fields(*field_names, &block) if field_names.empty? _fields.select { |f| f.instance_eval(&block) }.each do |f| next if f.defined + f.defined = true f.order = _fields.count(&:defined) end @@ -117,6 +118,10 @@ def visible_fields all_fields.collect { |f| f.with(bindings) }.select(&:visible?).sort_by { |f| [f.order, i += 1] } # stable sort, damn end + def possible_fields + _fields(true) + end + protected # Raw fields. @@ -126,7 +131,7 @@ def _fields(readonly = false) return @_fields if @_fields return @_ro_fields if readonly && @_ro_fields - if self.class == RailsAdmin::Config::Sections::Base + if instance_of?(RailsAdmin::Config::Sections::Base) @_ro_fields = @_fields = RailsAdmin::Config::Fields.factory(self) else # parent is RailsAdmin::Config::Model, recursion is on Section's classes diff --git a/lib/rails_admin/config/has_groups.rb b/lib/rails_admin/config/has_groups.rb index a9cb739124..b989e73bad 100644 --- a/lib/rails_admin/config/has_groups.rb +++ b/lib/rails_admin/config/has_groups.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/fields/group' module RailsAdmin diff --git a/lib/rails_admin/config/hideable.rb b/lib/rails_admin/config/hideable.rb index 8f8f699ca6..2b8fa68948 100644 --- a/lib/rails_admin/config/hideable.rb +++ b/lib/rails_admin/config/hideable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config # Defines a visibility configuration @@ -16,7 +18,7 @@ def hidden? # Writer to hide object. def hide(&block) - visible block ? proc { false == instance_eval(&block) } : false + visible block ? proc { instance_eval(&block) == false } : false end # Writer to show field. diff --git a/lib/rails_admin/config/inspectable.rb b/lib/rails_admin/config/inspectable.rb index e43ef5fd26..0028fe1d93 100644 --- a/lib/rails_admin/config/inspectable.rb +++ b/lib/rails_admin/config/inspectable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Inspectable @@ -30,9 +32,7 @@ def instance_variable_name(variable) end def set_named_instance_variables - unless defined?(self.class::NAMED_INSTANCE_VARIABLES) - self.class.const_set('NAMED_INSTANCE_VARIABLES', []) - end + self.class.const_set('NAMED_INSTANCE_VARIABLES', []) unless defined?(self.class::NAMED_INSTANCE_VARIABLES) end end end diff --git a/lib/rails_admin/config/lazy_model.rb b/lib/rails_admin/config/lazy_model.rb index 63bcfd3cdd..1e7256711e 100644 --- a/lib/rails_admin/config/lazy_model.rb +++ b/lib/rails_admin/config/lazy_model.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/model' module RailsAdmin @@ -6,11 +8,15 @@ class LazyModel < BasicObject def initialize(entity, &block) @entity = entity @deferred_blocks = [*block] - @existing_blocks = [] + @initialized = false end def add_deferred_block(&block) - @deferred_blocks << block + if @initialized + @model.instance_eval(&block) + else + @deferred_blocks << block + end end def target @@ -42,17 +48,17 @@ def target # : # end # - # Thus, sort all blocks to excute for a resource by Proc.source_path, + # Thus, sort all blocks to execute for a resource by Proc.source_path, # to guarantee that blocks from 'config/initializers' evaluate before # blocks defined within a model class. unless @deferred_blocks.empty? - @existing_blocks += @deferred_blocks - @existing_blocks. - partition { |block| block.source_location.first =~ %r{config\/initializers} }. + @deferred_blocks. + partition { |block| block.source_location.first =~ %r{config/initializers} }. flatten. each { |block| @model.instance_eval(&block) } @deferred_blocks = [] end + @initialized = true @model end @@ -60,7 +66,7 @@ def method_missing(method_name, *args, &block) target.send(method_name, *args, &block) end - def respond_to?(method_name, include_private = false) + def respond_to_missing?(method_name, include_private = false) super || target.respond_to?(method_name, include_private) end end diff --git a/lib/rails_admin/config/model.rb b/lib/rails_admin/config/model.rb index 84a2ac7cf8..68688ec747 100644 --- a/lib/rails_admin/config/model.rb +++ b/lib/rails_admin/config/model.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config' require 'rails_admin/config/proxyable' require 'rails_admin/config/configurable' @@ -21,30 +23,33 @@ class Model include RailsAdmin::Config::Sections include RailsAdmin::Config::Inspectable - attr_reader :abstract_model + attr_reader :abstract_model, :parent, :root attr_accessor :groups - attr_reader :parent, :root - NAMED_INSTANCE_VARIABLES = [:@parent, :@root].freeze + NAMED_INSTANCE_VARIABLES = %i[@parent @root].freeze def initialize(entity) @parent = nil @root = self - @abstract_model = begin - if entity.is_a?(RailsAdmin::AbstractModel) + @abstract_model = + case entity + when RailsAdmin::AbstractModel entity - elsif entity.is_a?(Class) || entity.is_a?(String) || entity.is_a?(Symbol) + when Class, String RailsAdmin::AbstractModel.new(entity) + when Symbol + RailsAdmin::AbstractModel.new(entity.to_s) else RailsAdmin::AbstractModel.new(entity.class) end - end + @groups = [RailsAdmin::Config::Fields::Group.new(self, :default).tap { |g| g.label { I18n.translate('admin.form.basic_info') } }] end def excluded? return @excluded if defined?(@excluded) + @excluded = !RailsAdmin::AbstractModel.all.collect(&:model_name).include?(abstract_model.try(:model_name)) end @@ -81,23 +86,30 @@ def pluralize(count) register_instance_option :parent do @parent_model ||= begin klass = abstract_model.model.superclass - klass = nil if klass.to_s.in?(%w(Object BasicObject ActiveRecord::Base)) + klass = nil if klass.to_s.in?(%w[Object BasicObject ActiveRecord::Base]) klass end end register_instance_option :navigation_label do - @navigation_label ||= begin + @navigation_label ||= if (parent_module = abstract_model.model.try(:module_parent) || abstract_model.model.try!(:parent)) != Object parent_module.to_s end - end end register_instance_option :navigation_icon do nil end + register_instance_option :scope do + abstract_model.scoped + end + + register_instance_option :last_created_at do + abstract_model.model.last.try(:created_at) if abstract_model.properties.detect { |c| c.name == :created_at } + end + # Act as a proxy for the base section configuration that actually # store the configurations. def method_missing(method_name, *args, &block) diff --git a/lib/rails_admin/config/proxyable.rb b/lib/rails_admin/config/proxyable.rb index 766f9f53a7..61551e1de1 100644 --- a/lib/rails_admin/config/proxyable.rb +++ b/lib/rails_admin/config/proxyable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/proxyable/proxy' module RailsAdmin module Config diff --git a/lib/rails_admin/config/proxyable/proxy.rb b/lib/rails_admin/config/proxyable/proxy.rb index ff631f8600..7913b57371 100644 --- a/lib/rails_admin/config/proxyable/proxy.rb +++ b/lib/rails_admin/config/proxyable/proxy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Config module Proxyable diff --git a/lib/rails_admin/config/sections.rb b/lib/rails_admin/config/sections.rb index 5b28dfe0db..21a47349fe 100644 --- a/lib/rails_admin/config/sections.rb +++ b/lib/rails_admin/config/sections.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/core_ext/string/inflections' require 'rails_admin/config/sections/base' require 'rails_admin/config/sections/edit' @@ -25,7 +27,7 @@ def self.included(klass) section = RailsAdmin::Config::Sections.const_get(name) name = name.to_s.underscore.to_sym klass.send(:define_method, name) do |&block| - @sections = {} unless @sections + @sections ||= {} @sections[name] = section.new(self) unless @sections[name] @sections[name].instance_eval(&block) if block @sections[name] diff --git a/lib/rails_admin/config/sections/base.rb b/lib/rails_admin/config/sections/base.rb index cdb8a5ffe1..718d55471e 100644 --- a/lib/rails_admin/config/sections/base.rb +++ b/lib/rails_admin/config/sections/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/proxyable' require 'rails_admin/config/configurable' require 'rails_admin/config/inspectable' @@ -18,10 +20,9 @@ class Base include RailsAdmin::Config::HasGroups include RailsAdmin::Config::HasDescription - attr_reader :abstract_model - attr_reader :parent, :root + attr_reader :abstract_model, :parent, :root - NAMED_INSTANCE_VARIABLES = [:@parent, :@root, :@abstract_model].freeze + NAMED_INSTANCE_VARIABLES = %i[@parent @root @abstract_model].freeze def initialize(parent) @parent = parent diff --git a/lib/rails_admin/config/sections/create.rb b/lib/rails_admin/config/sections/create.rb index 1bee8d1695..b2921cc20d 100644 --- a/lib/rails_admin/config/sections/create.rb +++ b/lib/rails_admin/config/sections/create.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/sections/edit' module RailsAdmin diff --git a/lib/rails_admin/config/sections/edit.rb b/lib/rails_admin/config/sections/edit.rb index a6ff8e0f67..dd910a32ec 100644 --- a/lib/rails_admin/config/sections/edit.rb +++ b/lib/rails_admin/config/sections/edit.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/sections/base' module RailsAdmin diff --git a/lib/rails_admin/config/sections/export.rb b/lib/rails_admin/config/sections/export.rb index df93bf3e5c..1ee3c45eb2 100644 --- a/lib/rails_admin/config/sections/export.rb +++ b/lib/rails_admin/config/sections/export.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/sections/base' module RailsAdmin diff --git a/lib/rails_admin/config/sections/list.rb b/lib/rails_admin/config/sections/list.rb index 1d8f1db186..ae5893ac7e 100644 --- a/lib/rails_admin/config/sections/list.rb +++ b/lib/rails_admin/config/sections/list.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/sections/base' module RailsAdmin @@ -28,12 +30,12 @@ class List < RailsAdmin::Config::Sections::Base nil end - register_instance_option :sort_by do - parent.abstract_model.primary_key + register_instance_option :search_help do + nil end - register_instance_option :sort_reverse? do - true # By default show latest first + register_instance_option :sort_by do + parent.abstract_model.primary_key end register_instance_option :scopes do @@ -44,23 +46,16 @@ class List < RailsAdmin::Config::Sections::Base '' end - register_instance_option :sidescroll do - nil + register_deprecated_instance_option :sidescroll do + ActiveSupport::Deprecation.warn('The sidescroll configuration option was removed, it is always enabled now.') + end + + def fields_for_table + visible_fields.partition(&:sticky?).flatten end - def sidescroll_frozen_columns - global_config = RailsAdmin::Config.sidescroll - model_config = sidescroll - enabled = model_config.nil? ? global_config : model_config - if enabled - num_frozen = model_config[:num_frozen_columns] if model_config.is_a?(Hash) - unless num_frozen - num_frozen = global_config[:num_frozen_columns] if global_config.is_a?(Hash) - num_frozen ||= 3 # by default, freeze checkboxes, links & first property (usually primary key / id?) - num_frozen -= 1 unless checkboxes? # model config should be explicit about this, only adjust if using global config - end - num_frozen - end + register_deprecated_instance_option :sort_reverse do + ActiveSupport::Deprecation.warn('The sort_reverse configuration option is deprecated and has no effect.') end end end diff --git a/lib/rails_admin/config/sections/modal.rb b/lib/rails_admin/config/sections/modal.rb index 0135efc0c3..a9f1379155 100644 --- a/lib/rails_admin/config/sections/modal.rb +++ b/lib/rails_admin/config/sections/modal.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/sections/edit' module RailsAdmin diff --git a/lib/rails_admin/config/sections/nested.rb b/lib/rails_admin/config/sections/nested.rb index 998c5b12c5..7db09a4cbf 100644 --- a/lib/rails_admin/config/sections/nested.rb +++ b/lib/rails_admin/config/sections/nested.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/sections/edit' module RailsAdmin diff --git a/lib/rails_admin/config/sections/show.rb b/lib/rails_admin/config/sections/show.rb index 789697a5a3..a015b29a3c 100644 --- a/lib/rails_admin/config/sections/show.rb +++ b/lib/rails_admin/config/sections/show.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/sections/base' module RailsAdmin diff --git a/lib/rails_admin/config/sections/update.rb b/lib/rails_admin/config/sections/update.rb index d5c082bb04..1a8e56a4d9 100644 --- a/lib/rails_admin/config/sections/update.rb +++ b/lib/rails_admin/config/sections/update.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/config/sections/edit' module RailsAdmin diff --git a/lib/rails_admin/engine.rb b/lib/rails_admin/engine.rb index 39def5cff9..3995e4f51d 100644 --- a/lib/rails_admin/engine.rb +++ b/lib/rails_admin/engine.rb @@ -1,68 +1,84 @@ -require 'jquery-rails' -require 'jquery-ui-rails' +# frozen_string_literal: true + require 'kaminari' require 'nested_form' -require 'rack-pjax' require 'rails' require 'rails_admin' -require 'remotipart' +require 'rails_admin/extensions/url_for_extension' +require 'rails_admin/version' +require 'turbo-rails' module RailsAdmin class Engine < Rails::Engine isolate_namespace RailsAdmin + attr_accessor :importmap + config.action_dispatch.rescue_responses['RailsAdmin::ActionNotAllowed'] = :forbidden - initializer 'RailsAdmin precompile hook', group: :all do |app| - app.config.assets.precompile += %w( - rails_admin/rails_admin.js - rails_admin/rails_admin.css - rails_admin/jquery.colorpicker.js - rails_admin/jquery.colorpicker.css - ) + initializer 'RailsAdmin load UrlForExtension' do + RailsAdmin::Engine.routes.singleton_class.prepend(RailsAdmin::Extensions::UrlForExtension) end - initializer 'RailsAdmin setup middlewares' do |app| - app.config.middleware.use Rack::Pjax - end + initializer 'RailsAdmin reload config in development' do |app| + config.initializer_path = app.root.join('config/initializers/rails_admin.rb') + + unless Rails.application.config.cache_classes + ActiveSupport::Reloader.before_class_unload do + RailsAdmin::Config.reload! + end - initializer 'RailsAdmin reload config in development' do - if Rails.application.config.cache_classes - if defined?(ActiveSupport::Reloader) - ActiveSupport::Reloader.before_class_unload do - RailsAdmin::Config.reset_all_models - end - # else - # For Rails 4 not implemented + reloader = app.config.file_watcher.new([config.initializer_path], []) do + # Do nothing, ActiveSupport::Reloader will trigger class_unload! anyway end + + app.reloaders << reloader + app.reloader.to_run do + reloader.execute_if_updated { require_unload_lock! } + end + reloader.execute end end - rake_tasks do - Dir[File.join(File.dirname(__FILE__), '../tasks/*.rake')].each { |f| load f } + initializer 'RailsAdmin precompile hook', group: :all do |app| + case RailsAdmin.config.asset_source + when :sprockets + app.config.assets.precompile += %w[ + rails_admin/application.js + rails_admin/application.css + ] + app.config.assets.paths << RailsAdmin::Engine.root.join('src') + require 'rails_admin/support/es_module_processor' + Sprockets.register_bundle_processor 'application/javascript', RailsAdmin::Support::ESModuleProcessor + when :importmap + self.importmap = Importmap::Map.new.draw(app.root.join('config/importmap.rails_admin.rb')) + end end # Check for required middlewares, users may forget to use them in Rails API mode config.after_initialize do |app| has_session_store = app.config.middleware.to_a.any? do |m| m.klass.try(:<=, ActionDispatch::Session::AbstractStore) || + m.klass.try(:<=, ActionDispatch::Session::AbstractSecureStore) || m.klass.name =~ /^ActionDispatch::Session::/ end loaded = app.config.middleware.to_a.map(&:name) - required = %w(ActionDispatch::Cookies ActionDispatch::Flash Rack::MethodOverride) + required = %w[ActionDispatch::Cookies ActionDispatch::Flash Rack::MethodOverride] missing = required - loaded unless missing.empty? && has_session_store configs = missing.map { |m| "config.middleware.use #{m}" } configs << "config.middleware.use #{app.config.session_store.try(:name) || 'ActionDispatch::Session::CookieStore'}, #{app.config.session_options}" unless has_session_store - raise <<-EOM -Required middlewares for RailsAdmin are not added -To fix this, add + raise <<~ERROR + Required middlewares for RailsAdmin are not added + To fix this, add - #{configs.join("\n ")} + #{configs.join("\n ")} -to config/application.rb. - EOM + to config/application.rb. + ERROR end + + RailsAdmin::Version.warn_with_js_version end end end diff --git a/lib/rails_admin/extension.rb b/lib/rails_admin/extension.rb index 3f963cc02e..591dfbba5b 100644 --- a/lib/rails_admin/extension.rb +++ b/lib/rails_admin/extension.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/extensions/controller_extension' module RailsAdmin @@ -15,27 +17,19 @@ def self.add_extension(extension_key, extension_definition, options = {}) EXTENSIONS << extension_key - if options[:authorization] - AUTHORIZATION_ADAPTERS[extension_key] = extension_definition::AuthorizationAdapter - end + AUTHORIZATION_ADAPTERS[extension_key] = extension_definition::AuthorizationAdapter if options[:authorization] - if options[:configuration] - CONFIGURATION_ADAPTERS[extension_key] = extension_definition::ConfigurationAdapter - end + CONFIGURATION_ADAPTERS[extension_key] = extension_definition::ConfigurationAdapter if options[:configuration] - if options[:auditing] - AUDITING_ADAPTERS[extension_key] = extension_definition::AuditingAdapter - end + AUDITING_ADAPTERS[extension_key] = extension_definition::AuditingAdapter if options[:auditing] end # Setup all extensions for testing def self.setup_all_extensions (AUTHORIZATION_ADAPTERS.values + AUDITING_ADAPTERS.values).each do |klass| - begin - klass.setup if klass.respond_to? :setup - rescue # rubocop:disable Lint/HandleExceptions, Style/RescueStandardError - # ignore errors - end + klass.setup if klass.respond_to? :setup + rescue # rubocop:disable Style/RescueStandardError + # ignore errors end end end diff --git a/lib/rails_admin/extensions/cancancan.rb b/lib/rails_admin/extensions/cancancan.rb index 9a639d8961..f742c85e59 100644 --- a/lib/rails_admin/extensions/cancancan.rb +++ b/lib/rails_admin/extensions/cancancan.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/extensions/cancancan/authorization_adapter' RailsAdmin.add_extension(:cancancan, RailsAdmin::Extensions::CanCanCan, authorization: true) diff --git a/lib/rails_admin/extensions/cancancan/authorization_adapter.rb b/lib/rails_admin/extensions/cancancan/authorization_adapter.rb index 6d239f0caf..86c1befb68 100644 --- a/lib/rails_admin/extensions/cancancan/authorization_adapter.rb +++ b/lib/rails_admin/extensions/cancancan/authorization_adapter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Extensions module CanCanCan @@ -7,18 +9,33 @@ module ControllerExtension def current_ability # use _current_user instead of default current_user so it works with # whatever current user method is defined with RailsAdmin - @current_ability ||= @ability.new(_current_user) + @current_ability ||= ability_class.new(_current_user) end end + include RailsAdmin::Config::Configurable + + def self.setup + RailsAdmin::Extensions::ControllerExtension.include ControllerExtension + end + # See the +authorize_with+ config method for where the initialization happens. - def initialize(controller, ability = ::Ability) + def initialize(controller, ability = nil, &block) @controller = controller - @controller.instance_variable_set '@ability', ability - @controller.extend ControllerExtension + ability_class { ability } if ability + instance_eval(&block) if block + + adapter = self + ControllerExtension.define_method(:ability_class) do + adapter.ability_class + end @controller.current_ability.authorize! :access, :rails_admin end + register_instance_option :ability_class do + Ability + end + # This method is called in every controller action and should raise an exception # when the authorization fails. The first argument is the name of the controller # action as a symbol (:create, :bulk_delete, etc.). The second argument is the @@ -26,6 +43,7 @@ def initialize(controller, ability = ::Ability) # instance if it is available. def authorize(action, abstract_model = nil, model_object = nil) return unless action + action, subject = resolve_action_and_subject(action, abstract_model, model_object) @controller.current_ability.authorize!(action, subject) end @@ -36,6 +54,7 @@ def authorize(action, abstract_model = nil, model_object = nil) # return a boolean whereas +authorize+ will raise an exception when not authorized. def authorized?(action, abstract_model = nil, model_object = nil) return unless action + action, subject = resolve_action_and_subject(action, abstract_model, model_object) @controller.current_ability.can?(action, subject) end @@ -51,13 +70,13 @@ def query(action, abstract_model) # records. It should return a hash of attributes which match what the user # is authorized to create. def attributes_for(action, abstract_model) - @controller.current_ability.attributes_for(action, abstract_model && abstract_model.model) + @controller.current_ability.attributes_for(action, abstract_model&.model) end private def resolve_action_and_subject(action, abstract_model, model_object) - subject = model_object || abstract_model && abstract_model.model + subject = model_object || abstract_model&.model if subject [action, subject] else diff --git a/lib/rails_admin/extensions/controller_extension.rb b/lib/rails_admin/extensions/controller_extension.rb index ea135a48f1..9907793f46 100644 --- a/lib/rails_admin/extensions/controller_extension.rb +++ b/lib/rails_admin/extensions/controller_extension.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Extensions module ControllerExtension diff --git a/lib/rails_admin/extensions/history.rb b/lib/rails_admin/extensions/history.rb deleted file mode 100644 index 8d579008a2..0000000000 --- a/lib/rails_admin/extensions/history.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'rails_admin/extensions/history/auditing_adapter' - -RailsAdmin.add_extension(:history, RailsAdmin::Extensions::History, auditing: true) diff --git a/lib/rails_admin/extensions/history/auditing_adapter.rb b/lib/rails_admin/extensions/history/auditing_adapter.rb deleted file mode 100644 index 6326d067e4..0000000000 --- a/lib/rails_admin/extensions/history/auditing_adapter.rb +++ /dev/null @@ -1,37 +0,0 @@ -module RailsAdmin - module Extensions - module History - class AuditingAdapter - def initialize(controller, user_class = User) - @controller = controller - @user_class = user_class.to_s.constantize - require 'rails_admin/extensions/history/history' - end - - def latest(count = 100) - ::RailsAdmin::History.latest(count) - end - - def delete_object(object, model, user) - ::RailsAdmin::History.create_history_item('delete', object, model, user) - end - - def update_object(object, model, user, changes) - ::RailsAdmin::History.create_history_item(changes.collect { |k, v| "#{k}: #{v.collect(&:inspect).join(' -> ')}" }, object, model, user) - end - - def create_object(object, model, user) - ::RailsAdmin::History.create_history_item('new', object, model, user) - end - - def listing_for_model(model, query, sort, sort_reverse, all, page, per_page = (RailsAdmin::Config.default_items_per_page || 20)) - ::RailsAdmin::History.history_for_model(model, query, sort, sort_reverse, all, page, per_page) - end - - def listing_for_object(model, object, query, sort, sort_reverse, all, page, per_page = (RailsAdmin::Config.default_items_per_page || 20)) - ::RailsAdmin::History.history_for_object(model, object, query, sort, sort_reverse, all, page, per_page) - end - end - end - end -end diff --git a/lib/rails_admin/extensions/history/history.rb b/lib/rails_admin/extensions/history/history.rb deleted file mode 100644 index d497ccdf43..0000000000 --- a/lib/rails_admin/extensions/history/history.rb +++ /dev/null @@ -1,44 +0,0 @@ -module RailsAdmin - class History < ActiveRecord::Base - self.table_name = :rails_admin_histories - - IGNORED_ATTRS = Set[:id, :created_at, :created_on, :deleted_at, :updated_at, :updated_on, :deleted_on] - - if defined?(ActiveModel::MassAssignmentSecurity) && ancestors.include?(ActiveModel::MassAssignmentSecurity) - attr_accessible :message, :item, :table, :username - end - - default_scope { order('id DESC') } - - class << self - def latest(count = 100) - limit(count) - end - - def create_history_item(message, object, abstract_model, user) - create(message: [message].flatten.join(', '), - item: object.id, - table: abstract_model.to_s, - username: user.try(:email)) - end - - def history_for_model(abstract_model, query, sort, sort_reverse, all, page, per_page = (RailsAdmin::Config.default_items_per_page || 20)) - history = where(table: abstract_model.to_s) - history_for_model_or_object(history, abstract_model, query, sort, sort_reverse, all, page, per_page) - end - - def history_for_object(abstract_model, object, query, sort, sort_reverse, all, page, per_page = (RailsAdmin::Config.default_items_per_page || 20)) - history = where(table: abstract_model.to_s, item: object.id) - history_for_model_or_object(history, abstract_model, query, sort, sort_reverse, all, page, per_page) - end - - protected - - def history_for_model_or_object(history, _abstract_model, query, sort, sort_reverse, all, page, per_page) - history = history.where('message LIKE ? OR username LIKE ?', "%#{query}%", "%#{query}%") if query - history = history.order(sort_reverse == 'true' ? "#{sort} DESC" : sort) if sort - all ? history : history.send(Kaminari.config.page_method_name, page.presence || '1').per(per_page) - end - end - end -end diff --git a/lib/rails_admin/extensions/paper_trail.rb b/lib/rails_admin/extensions/paper_trail.rb index 09d13bcd34..12b1cff032 100644 --- a/lib/rails_admin/extensions/paper_trail.rb +++ b/lib/rails_admin/extensions/paper_trail.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/extensions/paper_trail/auditing_adapter' RailsAdmin.add_extension(:paper_trail, RailsAdmin::Extensions::PaperTrail, auditing: true) diff --git a/lib/rails_admin/extensions/paper_trail/auditing_adapter.rb b/lib/rails_admin/extensions/paper_trail/auditing_adapter.rb index b710d67bcf..a9834aa616 100644 --- a/lib/rails_admin/extensions/paper_trail/auditing_adapter.rb +++ b/lib/rails_admin/extensions/paper_trail/auditing_adapter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/core_ext/string/strip' module RailsAdmin @@ -11,7 +13,7 @@ def initialize(version, user_class = User) def message @message = @version.event - @version.respond_to?(:changeset) && @version.changeset.present? ? @message + ' [' + @version.changeset.to_a.collect { |c| c[0] + ' = ' + c[1][1].to_s }.join(', ') + ']' : @message + @version.respond_to?(:changeset) && @version.changeset.present? ? @message + ' [' + @version.changeset.to_a.collect { |c| "#{c[0]} = #{c[1][1]}" }.join(', ') + ']' : @message end def created_at @@ -23,7 +25,11 @@ def table end def username - (@user_class.find(@version.whodunnit).try(:email) rescue nil) || @version.whodunnit + begin + @user_class.find(@version.whodunnit).try(:email) + rescue StandardError + nil + end || @version.whodunnit end def item @@ -45,42 +51,63 @@ class AuditingAdapter created_at: :created_at, message: :event, }.freeze - E_VERSION_MODEL_NOT_SET = <<-EOS.strip_heredoc.freeze + E_USER_CLASS_NOT_SET = <<~ERROR + Please set up PaperTrail's user class explicitly. + + config.audit_with :paper_trail do + user_class { User } + end + ERROR + E_VERSION_MODEL_NOT_SET = <<~ERROR Please set up PaperTrail's version model explicitly. - config.audit_with :paper_trail, 'User', 'PaperTrail::Version' + config.audit_with :paper_trail do + version_class { PaperTrail::Version } + end If you have configured a model to use a custom version class (https://github.com/paper-trail-gem/paper_trail#6a-custom-version-classes) - that configuration will take precedence over what you specify in - `audit_with`. - EOS + that configuration will take precedence over what you specify in `audit_with`. + ERROR + + include RailsAdmin::Config::Configurable def self.setup - raise('PaperTrail not found') unless defined?(::PaperTrail) - RailsAdmin::Extensions::ControllerExtension.send(:include, ControllerExtension) + raise 'PaperTrail not found' unless defined?(::PaperTrail) + + RailsAdmin::Extensions::ControllerExtension.include ControllerExtension end - def initialize(controller, user_class = 'User', version_class = '::Version') + def initialize(controller, user_class_name = nil, version_class_name = nil, &block) @controller = controller - @controller.send(:set_paper_trail_whodunnit) if @controller - begin - @user_class = user_class.to_s.constantize - rescue NameError - raise "Please set up Papertrail's user model explicitly. Ex: config.audit_with :paper_trail, 'User'" - end + @controller&.send(:set_paper_trail_whodunnit) - begin - @version_class = version_class.to_s.constantize - rescue NameError - raise E_VERSION_MODEL_NOT_SET - end + user_class { user_class_name.to_s.constantize } if user_class_name + version_class { version_class_name.to_s.constantize } if version_class_name + + instance_eval(&block) if block + end + + register_instance_option :user_class do + User + rescue NameError + raise E_USER_CLASS_NOT_SET + end + + register_instance_option :version_class do + PaperTrail::Version + rescue NameError + raise E_VERSION_MODEL_NOT_SET + end + + register_instance_option :sort_by do + {id: :desc} end def latest(count = 100) - @version_class. - order(id: :desc).includes(:item).limit(count). - collect { |version| VersionProxy.new(version, @user_class) } + version_class. + order(sort_by).includes(:item).limit(count). + collect { |version| VersionProxy.new(version, user_class) } end def delete_object(_object, _model, _user) @@ -107,26 +134,26 @@ def listing_for_object(model, object, query, sort, sort_reverse, all, page, per_ # - model - a RailsAdmin::AbstractModel def listing_for_model_or_object(model, object, query, sort, sort_reverse, all, page, per_page) - if sort.present? - sort = COLUMN_MAPPING[sort.to_sym] - else - sort = :created_at - sort_reverse = 'true' - end + sort = + if sort.present? + {COLUMN_MAPPING[sort.to_sym] => sort_reverse ? :desc : :asc} + else + sort_by + end current_page = page.presence || '1' - versions = object.nil? ? versions_for_model(model) : object.versions + versions = object.nil? ? versions_for_model(model) : object.public_send(model.model.versions_association_name) versions = versions.where('event LIKE ?', "%#{query}%") if query.present? - versions = versions.order(sort_reverse == 'true' ? "#{sort} DESC" : sort) - versions = all ? versions : versions.send(Kaminari.config.page_method_name, current_page).per(per_page) + versions = versions.order(sort) + versions = versions.send(Kaminari.config.page_method_name, current_page).per(per_page) unless all paginated_proxies = Kaminari.paginate_array([], total_count: versions.try(:total_count) || versions.count) paginated_proxies = paginated_proxies.send( paginated_proxies.respond_to?(Kaminari.config.page_method_name) ? Kaminari.config.page_method_name : :page, current_page, ).per(per_page) versions.each do |version| - paginated_proxies << VersionProxy.new(version, @user_class) + paginated_proxies << VersionProxy.new(version, user_class) end paginated_proxies end @@ -149,10 +176,10 @@ def versions_for_model(model) # classes](https://github.com/paper-trail-gem/paper_trail#6a-custom-version-classes) # # ```ruby - # has_paper_trail class_name: 'MyVersion' + # has_paper_trail versions: { class_name: 'MyVersion' } # ``` def version_class_for(model) - model.paper_trail_options[:class_name].try(:constantize) || @version_class + model.paper_trail.version_class end end end diff --git a/lib/rails_admin/extensions/pundit.rb b/lib/rails_admin/extensions/pundit.rb index f194e3f28e..ff9adf36ad 100644 --- a/lib/rails_admin/extensions/pundit.rb +++ b/lib/rails_admin/extensions/pundit.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/extensions/pundit/authorization_adapter' RailsAdmin.add_extension(:pundit, RailsAdmin::Extensions::Pundit, authorization: true) diff --git a/lib/rails_admin/extensions/pundit/authorization_adapter.rb b/lib/rails_admin/extensions/pundit/authorization_adapter.rb index fd46a47939..649957ecde 100644 --- a/lib/rails_admin/extensions/pundit/authorization_adapter.rb +++ b/lib/rails_admin/extensions/pundit/authorization_adapter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RailsAdmin module Extensions module Pundit @@ -7,7 +9,7 @@ module Pundit class AuthorizationAdapter # This method is called first time only and used for setup def self.setup - RailsAdmin::Extensions::ControllerExtension.send(:include, ::Pundit) + RailsAdmin::Extensions::ControllerExtension.include defined?(::Pundit::Authorization) ? ::Pundit::Authorization : ::Pundit end # See the +authorize_with+ config method for where the initialization happens. @@ -21,10 +23,9 @@ def initialize(controller) # AbstractModel instance that applies. The third argument is the actual model # instance if it is available. def authorize(action, abstract_model = nil, model_object = nil) - record = model_object || abstract_model && abstract_model.model - if action && !policy(record).send(action_for_pundit(action)) - raise ::Pundit::NotAuthorizedError.new("not allowed to #{action} this #{record}") - end + record = model_object || abstract_model&.model + raise ::Pundit::NotAuthorizedError.new("not allowed to #{action} this #{record}") if action && !policy(record).send(action_for_pundit(action)) + @controller.instance_variable_set(:@_pundit_policy_authorized, true) end @@ -33,7 +34,7 @@ def authorize(action, abstract_model = nil, model_object = nil) # This takes the same arguments as +authorize+. The difference is that this will # return a boolean whereas +authorize+ will raise an exception when not authorized. def authorized?(action, abstract_model = nil, model_object = nil) - record = model_object || abstract_model && abstract_model.model + record = model_object || abstract_model&.model policy(record).send(action_for_pundit(action)) if action end @@ -50,7 +51,7 @@ def query(_action, abstract_model) # records. It should return a hash of attributes which match what the user # is authorized to create. def attributes_for(action, abstract_model) - record = abstract_model && abstract_model.model + record = abstract_model&.model policy(record).try(:attributes_for, action) || {} end diff --git a/lib/rails_admin/extensions/url_for_extension.rb b/lib/rails_admin/extensions/url_for_extension.rb new file mode 100644 index 0000000000..8328b2dda1 --- /dev/null +++ b/lib/rails_admin/extensions/url_for_extension.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module RailsAdmin + module Extensions + module UrlForExtension + def url_for(options, *args) + case options[:id] + when Array + options[:id] = RailsAdmin.config.composite_keys_serializer.serialize(options[:id]) + end + super options, *args + end + end + end +end diff --git a/lib/rails_admin/support/composite_keys_serializer.rb b/lib/rails_admin/support/composite_keys_serializer.rb new file mode 100644 index 0000000000..3d79e0eb9e --- /dev/null +++ b/lib/rails_admin/support/composite_keys_serializer.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module RailsAdmin + module Support + module CompositeKeysSerializer + def self.serialize(keys) + keys.map { |key| key&.to_s&.gsub('_', '__') }.join('_') + end + + def self.deserialize(string) + string.split('_').map { |key| key&.gsub('__', '_') } + end + end + end +end diff --git a/lib/rails_admin/support/csv_converter.rb b/lib/rails_admin/support/csv_converter.rb index 5893336207..bbd7edbfc4 100644 --- a/lib/rails_admin/support/csv_converter.rb +++ b/lib/rails_admin/support/csv_converter.rb @@ -1,11 +1,13 @@ -# encoding: UTF-8 +# frozen_string_literal: true + require 'csv' module RailsAdmin class CSVConverter - def initialize(objects = [], schema = {}) + def initialize(objects = [], schema = nil) @fields = [] @associations = [] + schema ||= {} return self if (@objects = objects).blank? @@ -19,7 +21,9 @@ def initialize(objects = [], schema = {}) schema_include = schema.delete(:include) || {} @associations = schema_include.each_with_object({}) do |(key, values), hash| - association = association_for(key) + association = export_field_for(key) + next unless association&.association? + model_config = association.associated_model_config abstract_model = model_config.abstract_model methods = [(values[:only] || []) + (values[:methods] || [])].flatten.compact @@ -29,7 +33,7 @@ def initialize(objects = [], schema = {}) model: abstract_model.model, abstract_model: abstract_model, model_config: model_config, - fields: methods.collect { |m| export_fields_for(m, model_config).first }, + fields: methods.collect { |m| export_field_for(m, model_config) }.compact, } hash end @@ -37,18 +41,17 @@ def initialize(objects = [], schema = {}) def to_csv(options = {}) if CSV::VERSION == '3.0.2' - raise <<-MSG.gsub(/^\s+/, '') + raise <<~MSG CSV library bundled with Ruby 2.6.0 has encoding issue, please upgrade Ruby to 2.6.1 or later. https://github.com/ruby/csv/issues/62 MSG end + options = HashWithIndifferentAccess.new(options) encoding_to = Encoding.find(options[:encoding_to]) if options[:encoding_to].present? csv_string = generate_csv_string(options) - if encoding_to - csv_string = csv_string.encode(encoding_to, invalid: :replace, undef: :replace, replace: '?') - end + csv_string = csv_string.encode(encoding_to, invalid: :replace, undef: :replace, replace: '?') if encoding_to # Add a BOM for utf8 encodings, helps with utf8 auto-detect for some versions of Excel. # Don't add if utf8 but user don't want to touch input encoding: @@ -62,12 +65,8 @@ def to_csv(options = {}) private - def association_for(key) - export_fields_for(key).detect(&:association?) - end - - def export_fields_for(method, model_config = @model_config) - model_config.export.fields.select { |f| f.name == method } + def export_field_for(method, model_config = @model_config) + model_config.export.fields.detect { |f| f.name == method } end def generate_csv_string(options) diff --git a/lib/rails_admin/support/datetime.rb b/lib/rails_admin/support/datetime.rb index e175c1b66c..39345b9d16 100644 --- a/lib/rails_admin/support/datetime.rb +++ b/lib/rails_admin/support/datetime.rb @@ -1,98 +1,64 @@ -require 'rails_admin/support/i18n' +# frozen_string_literal: true module RailsAdmin module Support class Datetime - # Ruby format options as a key and momentjs format options as a value - MOMENTJS_TRANSLATIONS = { - '%a' => 'ddd', # The abbreviated weekday name ("Sun") - '%A' => 'dddd', # The full weekday name ("Sunday") - '%b' => 'MMM', # The abbreviated month name ("Jan") - '%B' => 'MMMM', # The full month name ("January") - '%d' => 'DD', # Day of the month (01..31) - '%-d' => 'D', # Day of the month (1..31) - '%D' => 'MM/DD/YY', # American date format mm/dd/yy - '%e' => 'D', # Day of the month (1..31) - '%F' => 'YY-MM-DD', # ISO 8601 date format - '%H' => 'HH', # Hour of the day, 24-hour clock (00..23) - '%I' => 'hh', # Hour of the day, 12-hour clock (01..12) - '%m' => 'MM', # Month of the year (01..12) - '%-m' => 'M', # Month of the year (1..12) - '%M' => 'mm', # Minute of the hour (00..59) - '%p' => 'A', # Meridian indicator ('AM' or 'PM') - '%S' => 'ss', # Second of the minute (00..60) - '%Y' => 'YYYY', # Year with century - '%y' => 'YY', # Year without a century (00..99) + # Ruby format options as a key and flatpickr format options as a value + FLATPICKR_TRANSLATIONS = { + '%A' => 'l', # The full weekday name ("Sunday") + '%a' => 'D', # The abbreviated weekday name ("Sun") + '%B' => 'F', # The full month name ("January") + '%b' => 'M', # The abbreviated month name ("Jan") + '%D' => 'm/d/y', # American date format mm/dd/yy + '%d' => 'd', # Day of the month (01..31) + '%-d' => 'j', # Day of the month (1..31) + '%e' => 'j', # Day of the month (1..31) + '%F' => 'Y-m-d', # ISO 8601 date format + '%H' => 'H', # Hour of the day, 24-hour clock (00..23) + '%-H' => 'H', # Hour of the day, 24-hour clock (0..23) + '%h' => 'M', # Same as %b + '%I' => 'G', # Hour of the day, 12-hour clock (01..12) + '%-I' => 'h', # Hour of the day, 12-hour clock (1..12) + '%k' => 'H', # Hour of the day, 24-hour clock (0..23) + '%l' => 'h', # Hour of the day, 12-hour clock (1..12) + '%-l' => 'h', # Hour of the day, 12-hour clock (1..12) + '%M' => 'i', # Minute of the hour (00..59) + '%-M' => 'i', # Minute of the hour (00..59) + '%m' => 'm', # Month of the year (01..12) + '%-m' => 'n', # Month of the year (1..12) + '%P' => 'K', # Meridian indicator ('am' or 'pm') + '%p' => 'K', # Meridian indicator ('AM' or 'PM') + '%R' => 'H:i', # 24-hour time (%H:%M) + '%r' => 'G:i:S K', # 12-hour time (%I:%M:%S %p) + '%S' => 'S', # Second of the minute (00..60) + '%-S' => 's', # Second of the minute (0..60) + '%s' => 'U', # Number of seconds since 1970-01-01 00:00:00 UTC. + '%T' => 'H:i:S', # 24-hour time (%H:%M:%S) + '%U' => 'W', # Week number of the year. The week starts with Sunday. (00..53) + '%w' => 'w', # Day of the week (Sunday is 0, 0..6) + '%X' => 'H:i:S', # Same as %T + '%x' => 'm/d/y', # Same as %D + '%Y' => 'Y', # Year with century + '%y' => 'y', # Year without a century (00..99) + '%%' => '%', }.freeze class << self - include RailsAdmin::Support::I18n - - def delocalize(date_string, format) - return date_string if ::I18n.locale.to_s == 'en' - format.to_s.scan(/%[AaBbp]/) do |match| + def to_flatpickr_format(strftime_format) + strftime_format.gsub(/(? adapter, 'database' => database, 'username' => username, - 'password' => ('postgresql' == adapter ? 'postgres' : ''), + 'password' => (adapter == 'postgresql' ? 'postgres' : ''), 'host' => '127.0.0.1', 'encoding' => 'utf8', 'pool' => 5, diff --git a/package.json b/package.json new file mode 100644 index 0000000000..f46a77587d --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "rails_admin", + "version": "3.2.0", + "description": "RailsAdmin is a Rails engine that provides an easy-to-use interface for managing your data.", + "homepage": "https://github.com/railsadminteam/rails_admin", + "license": "MIT", + "author": "Mitsuhiro Shibuya ", + "files": [ + "src" + ], + "main": "src/rails_admin/base.js", + "scripts": { + "link": "yarn link && cd spec/dummy_app && yarn link rails_admin", + "format": "prettier -w ." + }, + "dependencies": { + "@babel/runtime": "^7.16.7", + "@fortawesome/fontawesome-free": ">=5.15.0 <7.0.0", + "@hotwired/turbo-rails": "^7.1.0", + "@popperjs/core": "^2.11.0", + "@rails/ujs": "^6.1.4-1", + "bootstrap": "^5.1.3", + "flatpickr": "^4.6.9", + "jquery": "^3.6.0", + "jquery-ui": "^1.12.1 <1.14.0" + }, + "devDependencies": { + "prettier": "^2.4.1" + } +} diff --git a/rails_admin.gemspec b/rails_admin.gemspec index c6e5b87dd1..1905c682fd 100644 --- a/rails_admin.gemspec +++ b/rails_admin.gemspec @@ -1,32 +1,34 @@ -# coding: utf-8 -lib = File.expand_path('../lib', __FILE__) -$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'rails_admin/version' +# frozen_string_literal: true + +require_relative 'lib/rails_admin/version' Gem::Specification.new do |spec| # If you add a dependency, please maintain alphabetical order - spec.add_dependency 'builder', '~> 3.1' - spec.add_dependency 'haml', '>= 4.0', '< 6' - spec.add_dependency 'jquery-rails', ['>= 3.0', '< 5'] - spec.add_dependency 'jquery-ui-rails', ['>= 5.0', '< 7'] + spec.add_dependency 'activemodel-serializers-xml', '>= 1.0' + spec.add_dependency 'csv' spec.add_dependency 'kaminari', '>= 0.14', '< 2.0' spec.add_dependency 'nested_form', '~> 0.3' - spec.add_dependency 'rack-pjax', '>= 0.7' - spec.add_dependency 'rails', ['>= 5.0', '< 7'] - spec.add_dependency 'remotipart', '~> 1.3' - spec.add_dependency 'sassc-rails', ['>= 1.3', '< 3'] - spec.add_dependency 'activemodel-serializers-xml', '>= 1.0' + spec.add_dependency 'rails', ['>= 6.0', '< 8'] + spec.add_dependency 'turbo-rails', ['>= 1.0', '< 3'] spec.add_development_dependency 'bundler', '>= 1.0' spec.authors = ['Erik Michaels-Ober', 'Bogdan Gaza', 'Petteri Kaapa', 'Benoit Benezech', 'Mitsuhiro Shibuya'] spec.description = 'RailsAdmin is a Rails engine that provides an easy-to-use interface for managing your data.' spec.email = ['sferik@gmail.com', 'bogdan@cadmio.org', 'petteri.kaapa@gmail.com'] - spec.files = Dir['Gemfile', 'LICENSE.md', 'README.md', 'Rakefile', 'app/**/*', 'config/**/*', 'lib/**/*', 'public/**/*', 'vendor/**/*'] - spec.licenses = %w(MIT) - spec.homepage = 'https://github.com/sferik/rails_admin' + spec.files = Dir['Gemfile', 'LICENSE.md', 'README.md', 'Rakefile', 'package.json', 'app/**/*', 'config/**/*', 'lib/**/*', 'public/**/*', 'src/**/*', 'vendor/**/*'] + spec.licenses = %w[MIT] + spec.homepage = 'https://github.com/railsadminteam/rails_admin' spec.name = 'rails_admin' - spec.require_paths = %w(lib) - spec.required_ruby_version = '>= 2.2.2' + spec.require_paths = %w[lib] + spec.required_ruby_version = '>= 2.6.0' spec.required_rubygems_version = '>= 1.8.11' spec.summary = 'Admin for Rails' spec.version = RailsAdmin::Version + spec.post_install_message = <<~MSG + ### Upgrading RailsAdmin from 2.x.x to 3.x.x ### + + Due to introduction of Webpack/Webpacker support, some additional dependencies and configuration will be needed. + Running `bin/rails g rails_admin:install` will suggest required changes, based on the current setup of your app. + + For a complete list of changes, see https://github.com/railsadminteam/rails_admin/blob/master/CHANGELOG.md + MSG end diff --git a/spec/controllers/rails_admin/application_controller_spec.rb b/spec/controllers/rails_admin/application_controller_spec.rb index 9863c5f4e4..cb71c2d576 100644 --- a/spec/controllers/rails_admin/application_controller_spec.rb +++ b/spec/controllers/rails_admin/application_controller_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::ApplicationController, type: :controller do @@ -14,14 +16,14 @@ it 'works for static names' do RailsAdmin.config do |config| - config.main_app_name = %w(static value) + config.main_app_name = %w[static value] end - expect(controller.send(:_get_plugin_name)).to eq(%w(static value)) + expect(controller.send(:_get_plugin_name)).to eq(%w[static value]) end it 'works for dynamic names in the controller context' do RailsAdmin.config do |config| - config.main_app_name = proc { |controller| [Rails.application.engine_name.try(:titleize), controller.params[:action].titleize] } + config.main_app_name = proc { |controller| [Rails.application.engine_name&.titleize, controller.params[:action].titleize] } end controller.params[:action] = 'dashboard' expect(controller.send(:_get_plugin_name)).to eq(['Dummy App Application', 'Dashboard']) diff --git a/spec/controllers/rails_admin/main_controller_spec.rb b/spec/controllers/rails_admin/main_controller_spec.rb index 75fac5a3a3..c9f1b61c1f 100644 --- a/spec/controllers/rails_admin/main_controller_spec.rb +++ b/spec/controllers/rails_admin/main_controller_spec.rb @@ -1,4 +1,5 @@ -# encoding: utf-8 +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::MainController, type: :controller do @@ -8,9 +9,13 @@ def get(action, params) super action, params: params end + before do + controller.instance_variable_set :@action, RailsAdmin::Config::Actions.find(:index) + end + describe '#check_for_cancel' do before do - allow(controller).to receive(:back_or_index) { raise(StandardError.new('redirected back')) } + allow(controller).to receive(:back_or_index) { raise StandardError.new('redirected back') } end it 'redirects to back if params[:bulk_ids] is nil when params[:bulk_action] is present' do @@ -36,7 +41,47 @@ def get(action, params) it 'returns the option with no changes' do controller.params = {sort: 'team', model_name: 'players'} - expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))).to eq(sort: :"team.name", sort_reverse: true) + expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))).to eq(sort: :'team.name', sort_reverse: true) + end + end + + context 'when default sort_by points to a field with a table reference for sortable' do + before do + RailsAdmin.config('Player') do + base do + field :name do + sortable 'teams.name' + end + end + + list do + sort_by :name + end + end + end + + it 'returns the query referenced in the sortable' do + expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))).to eq(sort: 'teams.name', sort_reverse: true) + end + end + + context 'with a virtual field' do + before do + RailsAdmin.config('Player') do + base do + field :virtual do + sortable :name + end + end + + list do + sort_by :virtual + end + end + end + + it 'returns the query referenced in the sortable' do + expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))).to match(sort: /["`]?players["`]?\.["`]?name["`]?/, sort_reverse: true) end end @@ -45,21 +90,59 @@ def get(action, params) expect(controller.send(:get_sort_hash, RailsAdmin.config(Category))).to eq(sort: 'categories.parent_category_id', sort_reverse: true) end - context 'using mongoid, not supporting joins', mongoid: true do - it 'gives back the remote table with label name' do + context 'using mongoid', mongoid: true do + it 'gives back the remote table with label name, as it does not support joins' do controller.params = {sort: 'team', model_name: 'players'} - expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))).to eq(sort: 'players.team_id', sort_reverse: true) + expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))).to match(sort: 'players.team_id', sort_reverse: true) end end - context 'using active_record, supporting joins', active_record: true do + context 'using active_record', active_record: true do + let(:connection_config) do + if ActiveRecord::Base.respond_to?(:connection_db_config) + ActiveRecord::Base.connection_db_config.configuration_hash + else + ActiveRecord::Base.connection_config + end + end + it 'gives back the local column' do controller.params = {sort: 'team', model_name: 'players'} - expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))).to eq(sort: 'teams.name', sort_reverse: true) + expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))).to match(sort: /^["`]teams["`]\.["`]name["`]$/, sort_reverse: true) + end + + it 'quotes the table and column names it returns as :sort' do + controller.params = {sort: 'team', model_name: 'players'} + case connection_config[:adapter] + when 'mysql2' + expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))[:sort]).to eq '`teams`.`name`' + else + expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))[:sort]).to eq '"teams"."name"' + end end end end + describe '#bulk_action' do + before do + RailsAdmin.config do |config| + config.actions do + dashboard + index do + visible do + raise # This shouldn't be invoked + end + end + bulk_delete + end + end + end + + it 'retrieves actions using :bulkable scope' do + expect { post :bulk_action, params: {model_name: 'player', bulk_action: 'bulk_delete', bulk_ids: [1]} }.not_to raise_error + end + end + describe '#list_entries called from view' do before do @teams = FactoryBot.create_list(:team, 21) @@ -182,24 +265,65 @@ def get(action, params) end end + describe '#action_missing' do + it 'raises error when action is not found' do + expect(RailsAdmin::Config::Actions).to receive(:find).and_return(nil) + expect { get :index, model_name: 'player' }.to raise_error AbstractController::ActionNotFound + end + end + + describe '#respond_to_missing?' do + it 'returns the result based on existence of action' do + expect(controller.send(:respond_to_missing?, :index, false)).to be true + expect(controller.send(:respond_to_missing?, :invalid_action, false)).to be false + end + end + describe '#get_collection' do + let(:team) { FactoryBot.create :team } + let!(:player) { FactoryBot.create :player, team: team } + let(:model_config) { RailsAdmin.config(Team) } + let(:abstract_model) { model_config.abstract_model } before do - @team = FactoryBot.create(:team) - controller.params = {model_name: 'teams'} + controller.params = {model_name: 'team'} + end + + it 'performs eager-loading with `eager_load true`' do RailsAdmin.config Team do field :players do eager_load true end end - @model_config = RailsAdmin.config(Team) + expect(abstract_model).to receive(:all).with(hash_including(include: [:players]), nil).once.and_call_original + controller.send(:get_collection, model_config, nil, false).to_a end - it 'performs eager-loading for an association field with `eagar_load true`' do - scope = double('scope') - abstract_model = @model_config.abstract_model - allow(@model_config).to receive(:abstract_model).and_return(abstract_model) - expect(abstract_model).to receive(:all).with(hash_including(include: [:players]), scope).once - controller.send(:get_collection, @model_config, scope, false) + it 'performs eager-loading with custom eager_load value' do + RailsAdmin.config Team do + field :players do + eager_load players: :draft + end + end + expect(abstract_model).to receive(:all).with(hash_including(include: [{players: :draft}]), nil).once.and_call_original + controller.send(:get_collection, model_config, nil, false).to_a + end + + context 'on export' do + before do + controller.instance_variable_set :@action, RailsAdmin::Config::Actions.find(:export) + end + + it 'uses the export section' do + RailsAdmin.config Team do + export do + field :players do + eager_load true + end + end + end + expect(abstract_model).to receive(:all).with(hash_including(include: [:players]), nil).once.and_call_original + controller.send(:get_collection, model_config, nil, false).to_a + end end end @@ -207,8 +331,8 @@ def get(action, params) it "uses target model's primary key" do @user = FactoryBot.create :managing_user @team = FactoryBot.create :managed_team, user: @user - get :index, model_name: 'managing_user', source_object_id: @team.id, source_abstract_model: 'managing_user', associated_collection: 'teams', current_action: :create, compact: true, format: :json - expect(response.body).to match(/\"id\":\"#{@user.id}\"/) + get :index, model_name: 'managed_team', source_object_id: @user.id, source_abstract_model: 'managing_user', associated_collection: 'teams', current_action: :create, compact: true, format: :json + expect(response.body).to match(/"id":"#{@team.id}"/) end context 'as JSON' do @@ -227,7 +351,7 @@ def get(action, params) end controller(RailsAdmin::MainController) do - include ::Pundit + include defined?(::Pundit::Authorization) ? ::Pundit::Authorization : ::Pundit after_action :verify_authorized end @@ -245,27 +369,18 @@ def get(action, params) end describe 'sanitize_params_for!' do - context 'in France' do + context 'with datetime' do before do - I18n.locale = :fr ActionController::Parameters.permit_all_parameters = false - RailsAdmin.config FieldTest do - configure :datetime_field do - date_format { :default } - end - end - RailsAdmin.config Comment do configure :created_at do - date_format { :default } show end end RailsAdmin.config NestedFieldTest do configure :created_at do - date_format { :default } show end end @@ -273,19 +388,19 @@ def get(action, params) controller.params = ActionController::Parameters.new( 'field_test' => { 'unallowed_field' => "I shouldn't be here", - 'datetime_field' => '1 août 2010 00:00:00', + 'datetime_field' => '2010-08-01T00:00:00', 'nested_field_tests_attributes' => { 'new_1330520162002' => { 'comment_attributes' => { 'unallowed_field' => "I shouldn't be here", - 'created_at' => '2 août 2010 00:00:00', + 'created_at' => '2010-08-02T00:00:00', }, - 'created_at' => '3 août 2010 00:00:00', + 'created_at' => '2010-08-03T00:00:00', }, }, 'comment_attributes' => { 'unallowed_field' => "I shouldn't be here", - 'created_at' => '4 août 2010 00:00:00', + 'created_at' => '2010-08-04T00:00:00', }, }, ) @@ -294,7 +409,6 @@ def get(action, params) after do ActionController::Parameters.permit_all_parameters = true - I18n.locale = :en end it 'sanitize params recursively in nested forms' do @@ -329,15 +443,21 @@ def get(action, params) field :paperclip_asset do delete_method :delete_paperclip_asset end - field :active_storage_asset do - delete_method :remove_active_storage_asset - end if defined?(ActiveStorage) - field :active_storage_assets do - delete_method :remove_active_storage_assets - end if defined?(ActiveStorage) - field :shrine_asset do - delete_method :remove_shrine_asset - end if defined?(Shrine) + if defined?(ActiveStorage) + field :active_storage_asset do + delete_method :remove_active_storage_asset + end + end + if defined?(ActiveStorage) + field :active_storage_assets do + delete_method :remove_active_storage_assets + end + end + if defined?(Shrine) + field :shrine_asset do + delete_method :remove_shrine_asset + end + end end controller.params = HashWithIndifferentAccess.new( 'field_test' => { @@ -388,4 +508,70 @@ def get(action, params) ) end end + + describe 'back_or_index' do + before do + allow(controller).to receive(:index_path).and_return(index_path) + end + + let(:index_path) { '/' } + + it 'returns back to index when return_to is not defined' do + controller.params = {} + expect(controller.send(:back_or_index)).to eq(index_path) + end + + it 'returns back to return_to url when it starts with same protocol and host' do + return_to_url = "http://#{request.host}/teams" + controller.params = {return_to: return_to_url} + expect(controller.send(:back_or_index)).to eq(return_to_url) + end + + it 'returns back to return_to url when it contains a path' do + return_to_url = '/teams' + controller.params = {return_to: return_to_url} + expect(controller.send(:back_or_index)).to eq(return_to_url) + end + + it 'returns back to index path when return_to path does not start with slash' do + return_to_url = 'teams' + controller.params = {return_to: return_to_url} + expect(controller.send(:back_or_index)).to eq(index_path) + end + + it 'returns back to index path when return_to url does not start with full protocol' do + return_to_url = "#{request.host}/teams" + controller.params = {return_to: return_to_url} + expect(controller.send(:back_or_index)).to eq(index_path) + end + + it 'returns back to index path when return_to url starts with double slash' do + return_to_url = "//#{request.host}/teams" + controller.params = {return_to: return_to_url} + expect(controller.send(:back_or_index)).to eq(index_path) + end + + it 'returns back to index path when return_to url starts with triple slash' do + return_to_url = "///#{request.host}/teams" + controller.params = {return_to: return_to_url} + expect(controller.send(:back_or_index)).to eq(index_path) + end + + it 'returns back to index path when return_to url does not have host' do + return_to_url = 'http:///teams' + controller.params = {return_to: return_to_url} + expect(controller.send(:back_or_index)).to eq(index_path) + end + + it 'returns back to index path when return_to url starts with different protocol' do + return_to_url = "other://#{request.host}/teams" + controller.params = {return_to: return_to_url} + expect(controller.send(:back_or_index)).to eq(index_path) + end + + it 'returns back to index path when return_to does not start with the same protocol and host' do + controller.params = {return_to: "http://google.com?#{request.host}"} + expect(controller.send(:back_or_index)).to eq(index_path) + end + end end diff --git a/spec/dummy_app/.browserslistrc b/spec/dummy_app/.browserslistrc new file mode 100644 index 0000000000..e94f8140cc --- /dev/null +++ b/spec/dummy_app/.browserslistrc @@ -0,0 +1 @@ +defaults diff --git a/spec/dummy_app/.dockerignore b/spec/dummy_app/.dockerignore new file mode 100644 index 0000000000..9dc7e6a200 --- /dev/null +++ b/spec/dummy_app/.dockerignore @@ -0,0 +1,39 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ + +# Ignore bundler config. +/.bundle + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all environment files. +/.env* +!/.env.example + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets diff --git a/spec/dummy_app/.gitignore b/spec/dummy_app/.gitignore index dd71abbe4e..8a791fb421 100644 --- a/spec/dummy_app/.gitignore +++ b/spec/dummy_app/.gitignore @@ -7,6 +7,9 @@ # Ignore bundler config /.bundle +/app/assets/builds/* +!/app/assets/builds/.keep + # Ignore the default SQLite database. /db/*.sqlite3 @@ -15,3 +18,13 @@ /tmp /public/system + +/public/assets +/public/packs +/public/packs-test +/public/vite* +/node_modules +/yarn-error.log +/yarn.lock +yarn-debug.log* +.yarn-integrity diff --git a/spec/dummy_app/Dockerfile b/spec/dummy_app/Dockerfile new file mode 100644 index 0000000000..d7ab47d6d4 --- /dev/null +++ b/spec/dummy_app/Dockerfile @@ -0,0 +1,87 @@ +# syntax = docker/dockerfile:1 + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile +ARG RUBY_VERSION=3.1.2 +FROM ruby:$RUBY_VERSION-slim as base + +LABEL fly_launch_runtime="rails" + +# Rails app lives here +WORKDIR /rails + +# Set production environment +ENV RAILS_ENV="production" \ + BUNDLE_WITHOUT="development:test" + +# Update gems and bundler +RUN gem update --system --no-document && \ + gem install -N bundler + +# Install packages needed to install nodejs +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install Node.js +# ARG NODE_VERSION=18.16.0 +# ENV PATH=/usr/local/node/bin:$PATH +# RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \ +# /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \ +# rm -rf /tmp/node-build-master + +# Throw-away build stage to reduce size of final image +FROM base as build + +# Install packages needed to build gems and node modules +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential default-libmysqlclient-dev git libpq-dev libvips node-gyp pkg-config python-is-python3 + +# Build options +ENV PATH="/usr/local/node/bin:$PATH" + +# Install application gems +COPY --link Gemfile ./ +RUN sed -i "s/, path: '..\/..\/'//" Gemfile +RUN bundle install && \ + rm -rf ~/.bundle/ $BUNDLE_PATH/ruby/*/cache $BUNDLE_PATH/ruby/*/bundler/gems/*/.git + +# Install node modules +# COPY --link package.json ./ +# RUN npm install + +# Copy application code +COPY --link . . +RUN sed -i "s/, path: '..\/..\/'//" Gemfile + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN sed -i "/link_tree ..\/..\/..\//d" app/assets/config/manifest.js +RUN SECRET_KEY_BASE=DUMMY ./bin/rails assets:precompile + + +# Final stage for app image +FROM base + +# Install packages needed for deployment +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl default-mysql-client imagemagick libsqlite3-0 libvips postgresql-client && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Copy built artifacts: gems, application +COPY --from=build /usr/local/bundle /usr/local/bundle +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN useradd rails --create-home --shell /bin/bash && \ + chown -R rails:rails db log tmp public/system public/uploads +USER rails:rails + +# Deployment options +ENV RAILS_LOG_TO_STDOUT="1" \ + RAILS_SERVE_STATIC_FILES="true" + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD ["./bin/rails", "server"] diff --git a/spec/dummy_app/Gemfile b/spec/dummy_app/Gemfile index 38f1742206..5a16903ea3 100644 --- a/spec/dummy_app/Gemfile +++ b/spec/dummy_app/Gemfile @@ -1,6 +1,8 @@ +# frozen_string_literal: true + source 'https://rubygems.org' -gem 'rails', '>= 6.0.0' +gem 'rails', '>= 7.0.0' group :active_record do platforms :jruby do @@ -12,34 +14,30 @@ group :active_record do platforms :ruby, :mswin, :mingw do gem 'mysql2', '>= 0.3.14' gem 'pg', '>= 0.14' - gem 'sqlite3', '>= 1.3.0' + gem 'sqlite3', '~> 1.3' end - gem 'paper_trail', '>= 5.0' -end - -group :mongoid do - gem 'mongoid', ['>= 6.0', '< 8'] - gem 'kaminari-mongoid' - gem 'mongoid-paperclip', '>= 0.0.8', require: 'mongoid_paperclip' - gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' - gem 'shrine-mongoid', '~> 1.0' + gem 'paper_trail', '>= 12.0' end gem 'carrierwave', '>= 2.0.0.rc', '< 3.0' +gem 'cssbundling-rails', require: false gem 'devise', '>= 3.2' gem 'dragonfly', '~> 1.0' +gem 'importmap-rails', require: false gem 'mini_magick', '>= 3.4' -gem 'mlb', '>= 0.7' +gem 'mlb', '>= 0.7', github: 'mshibuya/mlb', branch: 'ruby-3' gem 'paperclip', '>= 3.4' gem 'rails_admin', path: '../../' gem 'shrine', '~> 3.0' +gem 'vite_rails', require: false +gem 'webpacker', require: false +gem 'webrick' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sassc-rails', '~> 2.1' - gem 'coffee-rails', '~> 4.0' # See https://github.com/sstephenson/execjs#readme for more supported runtimes # gem 'therubyracer' diff --git a/spec/dummy_app/Gemfile.rails6 b/spec/dummy_app/Gemfile.rails6 new file mode 100644 index 0000000000..260b684c14 --- /dev/null +++ b/spec/dummy_app/Gemfile.rails6 @@ -0,0 +1,49 @@ +source 'https://rubygems.org' + +gem 'rails', '>= 6.0.0' +gem 'webpacker', require: false + +group :active_record do + platforms :jruby do + gem 'activerecord-jdbcmysql-adapter', '>= 1.2' + gem 'activerecord-jdbcpostgresql-adapter', '>= 1.2' + gem 'activerecord-jdbcsqlite3-adapter', '>= 1.2' + end + + platforms :ruby, :mswin, :mingw do + gem 'mysql2', '>= 0.3.14' + gem 'pg', '>= 0.14' + gem 'sqlite3', '>= 1.3.0' + end + + gem 'paper_trail', '>= 12.0' +end + +group :mongoid do + gem 'carrierwave-mongoid', '>= 0.6.3', require: 'carrierwave/mongoid' + gem 'kaminari-mongoid' + gem 'mongoid', ['>= 6.0', '< 8'] + gem 'mongoid-paperclip', '>= 0.0.8', require: 'mongoid_paperclip' + gem 'shrine-mongoid', '~> 1.0' +end + +gem 'carrierwave', '>= 2.0.0.rc', '< 3.0' +gem 'devise', '>= 3.2' +gem 'dragonfly', '~> 1.0' +gem 'mini_magick', '>= 3.4' +gem 'mlb', '>= 0.7', github: 'mshibuya/mlb', branch: 'ruby-3' +gem 'paperclip', '>= 3.4' +gem 'rails_admin', path: '../../' +gem 'shrine', '~> 3.0' +gem 'webrick' + +# Gems used only for assets and not required +# in production environments by default. +group :assets do + gem 'sassc-rails', '~> 2.1' + + # See https://github.com/sstephenson/execjs#readme for more supported runtimes + # gem 'therubyracer' + + gem 'uglifier', '>= 1.3' +end diff --git a/spec/dummy_app/Procfile.dev b/spec/dummy_app/Procfile.dev new file mode 100644 index 0000000000..38f2b2dabb --- /dev/null +++ b/spec/dummy_app/Procfile.dev @@ -0,0 +1,3 @@ +web: bin/rails server -p 3000 +css: yarn build:css --watch +vite: bin/vite dev diff --git a/spec/dummy_app/Rakefile b/spec/dummy_app/Rakefile index 1187394dce..566ac3f002 100644 --- a/spec/dummy_app/Rakefile +++ b/spec/dummy_app/Rakefile @@ -1,7 +1,8 @@ -#!/usr/bin/env rake +# frozen_string_literal: true + # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require File.expand_path('../config/application', __FILE__) +require File.expand_path('config/application', __dir__) DummyApp::Application.load_tasks diff --git a/spec/dummy_app/app/active_record/abstract.rb b/spec/dummy_app/app/active_record/abstract.rb index c783084eb2..2264408887 100644 --- a/spec/dummy_app/app/active_record/abstract.rb +++ b/spec/dummy_app/app/active_record/abstract.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Abstract < ActiveRecord::Base self.abstract_class = true end diff --git a/spec/dummy_app/app/active_record/another_field_test.rb b/spec/dummy_app/app/active_record/another_field_test.rb index 0d19ac2570..1204ac0909 100644 --- a/spec/dummy_app/app/active_record/another_field_test.rb +++ b/spec/dummy_app/app/active_record/another_field_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AnotherFieldTest < ActiveRecord::Base has_many :nested_field_tests, inverse_of: :another_field_test end diff --git a/spec/dummy_app/app/active_record/ball.rb b/spec/dummy_app/app/active_record/ball.rb index d84138a411..a533986be7 100644 --- a/spec/dummy_app/app/active_record/ball.rb +++ b/spec/dummy_app/app/active_record/ball.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Ball < ActiveRecord::Base has_one :comment, as: :commentable diff --git a/spec/dummy_app/app/active_record/carrierwave_uploader.rb b/spec/dummy_app/app/active_record/carrierwave_uploader.rb index 3fadff5ce4..e4b50dbbd0 100644 --- a/spec/dummy_app/app/active_record/carrierwave_uploader.rb +++ b/spec/dummy_app/app/active_record/carrierwave_uploader.rb @@ -1,4 +1,5 @@ -# encoding: utf-8 +# frozen_string_literal: true + require 'mini_magick' class CarrierwaveUploader < CarrierWave::Uploader::Base # Include RMagick or ImageScience support: @@ -37,9 +38,9 @@ def store_dir # process scale: [50, 50] # end - # Add a white list of extensions which are allowed to be uploaded. + # Add an allowlist of extensions which are allowed to be uploaded. # For images you might use something like this: - # def extension_white_list + # def extension_allowlist # %w(jpg jpeg gif png) # end diff --git a/spec/dummy_app/app/active_record/category.rb b/spec/dummy_app/app/active_record/category.rb index 423ff8acb2..5c3d43e0d5 100644 --- a/spec/dummy_app/app/active_record/category.rb +++ b/spec/dummy_app/app/active_record/category.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Category < ActiveRecord::Base belongs_to :parent_category, class_name: 'Category', optional: true end diff --git a/spec/dummy_app/app/active_record/cms.rb b/spec/dummy_app/app/active_record/cms.rb index a868656d3e..bb97036cf3 100644 --- a/spec/dummy_app/app/active_record/cms.rb +++ b/spec/dummy_app/app/active_record/cms.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Cms def self.table_name_prefix 'cms_' diff --git a/spec/dummy_app/app/active_record/cms/basic_page.rb b/spec/dummy_app/app/active_record/cms/basic_page.rb index ce9ad6018c..e14fd75bad 100644 --- a/spec/dummy_app/app/active_record/cms/basic_page.rb +++ b/spec/dummy_app/app/active_record/cms/basic_page.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Cms class BasicPage < ActiveRecord::Base self.table_name = :cms_basic_pages diff --git a/spec/dummy_app/app/active_record/comment.rb b/spec/dummy_app/app/active_record/comment.rb index 11a6992765..68adbfc185 100644 --- a/spec/dummy_app/app/active_record/comment.rb +++ b/spec/dummy_app/app/active_record/comment.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Comment < ActiveRecord::Base include Taggable belongs_to :commentable, polymorphic: true, optional: true diff --git a/spec/dummy_app/app/active_record/comment/confirmed.rb b/spec/dummy_app/app/active_record/comment/confirmed.rb index 45ccf29407..b96fba3bff 100644 --- a/spec/dummy_app/app/active_record/comment/confirmed.rb +++ b/spec/dummy_app/app/active_record/comment/confirmed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Comment class Confirmed < Comment default_scope { where(content: 'something') } diff --git a/spec/dummy_app/app/active_record/concerns/taggable.rb b/spec/dummy_app/app/active_record/concerns/taggable.rb index be7c776ece..6293ab069b 100644 --- a/spec/dummy_app/app/active_record/concerns/taggable.rb +++ b/spec/dummy_app/app/active_record/concerns/taggable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Taggable extend ActiveSupport::Concern # dummy diff --git a/spec/dummy_app/app/active_record/deeply_nested_field_test.rb b/spec/dummy_app/app/active_record/deeply_nested_field_test.rb index c971a75614..7779eac210 100644 --- a/spec/dummy_app/app/active_record/deeply_nested_field_test.rb +++ b/spec/dummy_app/app/active_record/deeply_nested_field_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DeeplyNestedFieldTest < ActiveRecord::Base belongs_to :nested_field_test, inverse_of: :deeply_nested_field_tests end diff --git a/spec/dummy_app/app/active_record/division.rb b/spec/dummy_app/app/active_record/division.rb index f24ff09620..7fda495748 100644 --- a/spec/dummy_app/app/active_record/division.rb +++ b/spec/dummy_app/app/active_record/division.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Division < ActiveRecord::Base self.primary_key = :custom_id diff --git a/spec/dummy_app/app/active_record/draft.rb b/spec/dummy_app/app/active_record/draft.rb index dfbb951bed..18a22076f4 100644 --- a/spec/dummy_app/app/active_record/draft.rb +++ b/spec/dummy_app/app/active_record/draft.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Draft < ActiveRecord::Base belongs_to :team belongs_to :player diff --git a/spec/dummy_app/app/active_record/fan.rb b/spec/dummy_app/app/active_record/fan.rb index 1ec4e1effb..bd6bbf0959 100644 --- a/spec/dummy_app/app/active_record/fan.rb +++ b/spec/dummy_app/app/active_record/fan.rb @@ -1,5 +1,12 @@ +# frozen_string_literal: true + class Fan < ActiveRecord::Base has_and_belongs_to_many :teams + if ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) + has_many :fanships, inverse_of: :fan + has_one :fanship, inverse_of: :fan + end + validates_presence_of(:name) end diff --git a/spec/dummy_app/app/active_record/fanship.rb b/spec/dummy_app/app/active_record/fanship.rb new file mode 100644 index 0000000000..2bac5f5372 --- /dev/null +++ b/spec/dummy_app/app/active_record/fanship.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +if ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) + class Fanship < ActiveRecord::Base + self.table_name = :fans_teams + if defined?(CompositePrimaryKeys) + self.primary_keys = :fan_id, :team_id + else + self.primary_key = :fan_id, :team_id + end + if defined?(CompositePrimaryKeys) || ActiveRecord.gem_version >= Gem::Version.new('7.2') + has_many :favorite_players, foreign_key: %i[fan_id team_id], inverse_of: :fanship + else + has_many :favorite_players, query_constraints: %i[fan_id team_id], inverse_of: :fanship + end + + belongs_to :fan, inverse_of: :fanships, optional: true + belongs_to :team, optional: true + end +else + class Fanship; end +end diff --git a/spec/dummy_app/app/active_record/favorite_player.rb b/spec/dummy_app/app/active_record/favorite_player.rb new file mode 100644 index 0000000000..b91e26ab6b --- /dev/null +++ b/spec/dummy_app/app/active_record/favorite_player.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +if ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) + class FavoritePlayer < ActiveRecord::Base + if defined?(CompositePrimaryKeys) + self.primary_keys = :fan_id, :team_id, :player_id + else + self.primary_key = :fan_id, :team_id, :player_id + end + if defined?(CompositePrimaryKeys) || ActiveRecord.gem_version >= Gem::Version.new('7.2') + belongs_to :fanship, foreign_key: %i[fan_id team_id], inverse_of: :favorite_players + else + belongs_to :fanship, query_constraints: %i[fan_id team_id], inverse_of: :favorite_players + end + + belongs_to :player + end +end diff --git a/spec/dummy_app/app/active_record/field_test.rb b/spec/dummy_app/app/active_record/field_test.rb index be6f5d6e26..63b3c4b2db 100644 --- a/spec/dummy_app/app/active_record/field_test.rb +++ b/spec/dummy_app/app/active_record/field_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class FieldTest < ActiveRecord::Base has_many :nested_field_tests, dependent: :destroy, inverse_of: :field_test accepts_nested_attributes_for :nested_field_tests, allow_destroy: true @@ -7,6 +9,7 @@ class FieldTest < ActiveRecord::Base has_attached_file :paperclip_asset, styles: {thumb: '100x100>'} attr_accessor :delete_paperclip_asset + before_validation { self.paperclip_asset = nil if delete_paperclip_asset == '1' } ActiveRecord::Base.extend Dragonfly::Model @@ -15,17 +18,23 @@ class FieldTest < ActiveRecord::Base mount_uploader :carrierwave_asset, CarrierwaveUploader mount_uploaders :carrierwave_assets, CarrierwaveUploader - serialize :carrierwave_assets, JSON + if ActiveRecord.gem_version < Gem::Version.new('7.1') + serialize :carrierwave_assets, JSON + else + serialize :carrierwave_assets, coder: JSON + end if defined?(ActiveStorage) has_one_attached :active_storage_asset attr_accessor :remove_active_storage_asset + after_save { active_storage_asset.purge if remove_active_storage_asset == '1' } has_many_attached :active_storage_assets attr_accessor :remove_active_storage_assets + after_save do - Array(remove_active_storage_assets).each { |id| active_storage_assets.find_by_id(id).try(:purge) } + Array(remove_active_storage_assets).each { |id| active_storage_assets.find_by_id(id)&.purge } end end @@ -34,8 +43,13 @@ class FieldTest < ActiveRecord::Base has_rich_text :action_text_field if defined?(ActionText) - enum string_enum_field: {S: 's', M: 'm', L: 'l'} - enum integer_enum_field: [:small, :medium, :large] + if ActiveRecord.gem_version >= Gem::Version.new('7.0') + enum :string_enum_field, {S: 's', M: 'm', L: 'l'} + enum :integer_enum_field, %i[small medium large] + else + enum string_enum_field: {S: 's', M: 'm', L: 'l'} + enum integer_enum_field: %i[small medium large] + end validates :string_field, exclusion: {in: ['Invalid']} # to test file upload caching end diff --git a/spec/dummy_app/app/active_record/hardball.rb b/spec/dummy_app/app/active_record/hardball.rb index 3f7b98da4b..7de1a351db 100644 --- a/spec/dummy_app/app/active_record/hardball.rb +++ b/spec/dummy_app/app/active_record/hardball.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class Hardball < Ball end diff --git a/spec/dummy_app/app/active_record/image.rb b/spec/dummy_app/app/active_record/image.rb index 8d37e8833a..f45c5b74d5 100644 --- a/spec/dummy_app/app/active_record/image.rb +++ b/spec/dummy_app/app/active_record/image.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Image < ActiveRecord::Base has_attached_file :file, styles: {medium: '300x300>', thumb: '100x100>'} validates_attachment_presence :file diff --git a/spec/dummy_app/app/active_record/league.rb b/spec/dummy_app/app/active_record/league.rb index adf55a232d..c1b4c45623 100644 --- a/spec/dummy_app/app/active_record/league.rb +++ b/spec/dummy_app/app/active_record/league.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + class League < ActiveRecord::Base has_many :divisions, foreign_key: 'custom_league_id' has_many :teams, -> { readonly }, through: :divisions has_many :players, through: :teams + has_one :division, foreign_key: 'custom_league_id' validates_presence_of(:name) diff --git a/spec/dummy_app/app/active_record/managed_team.rb b/spec/dummy_app/app/active_record/managed_team.rb index fc789b716d..4c1f91a7f7 100644 --- a/spec/dummy_app/app/active_record/managed_team.rb +++ b/spec/dummy_app/app/active_record/managed_team.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ManagedTeam < Team belongs_to :user, class_name: 'ManagingUser', foreign_key: :manager, primary_key: :email, optional: true, inverse_of: :teams end diff --git a/spec/dummy_app/app/active_record/managing_user.rb b/spec/dummy_app/app/active_record/managing_user.rb index 87d36a90f8..91baf676dc 100644 --- a/spec/dummy_app/app/active_record/managing_user.rb +++ b/spec/dummy_app/app/active_record/managing_user.rb @@ -1,8 +1,6 @@ +# frozen_string_literal: true + class ManagingUser < User has_one :team, class_name: 'ManagedTeam', foreign_key: :manager, primary_key: :email, inverse_of: :user has_many :teams, class_name: 'ManagedTeam', foreign_key: :manager, primary_key: :email, inverse_of: :user - - def team_id=(id) - self.team = ManagedTeam.find_by_id(id) - end end diff --git a/spec/dummy_app/app/active_record/nested_fan.rb b/spec/dummy_app/app/active_record/nested_fan.rb new file mode 100644 index 0000000000..f49fc7f453 --- /dev/null +++ b/spec/dummy_app/app/active_record/nested_fan.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +if ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) + class NestedFan < Fan + accepts_nested_attributes_for :fanships + accepts_nested_attributes_for :fanship + end +end diff --git a/spec/dummy_app/app/active_record/nested_favorite_player.rb b/spec/dummy_app/app/active_record/nested_favorite_player.rb new file mode 100644 index 0000000000..a609b6646e --- /dev/null +++ b/spec/dummy_app/app/active_record/nested_favorite_player.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +if ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) + class NestedFavoritePlayer < FavoritePlayer + accepts_nested_attributes_for :fanship + end +end diff --git a/spec/dummy_app/app/active_record/nested_field_test.rb b/spec/dummy_app/app/active_record/nested_field_test.rb index c0aaa933d0..8873b1301b 100644 --- a/spec/dummy_app/app/active_record/nested_field_test.rb +++ b/spec/dummy_app/app/active_record/nested_field_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class NestedFieldTest < ActiveRecord::Base belongs_to :field_test, optional: true, inverse_of: :nested_field_tests belongs_to :another_field_test, optional: true, inverse_of: :nested_field_tests diff --git a/spec/dummy_app/app/active_record/paper_trail_test.rb b/spec/dummy_app/app/active_record/paper_trail_test.rb index 39b7ee725c..4675895ca1 100644 --- a/spec/dummy_app/app/active_record/paper_trail_test.rb +++ b/spec/dummy_app/app/active_record/paper_trail_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PaperTrailTest < ActiveRecord::Base has_paper_trail end diff --git a/spec/dummy_app/app/active_record/paper_trail_test/subclass_in_namespace.rb b/spec/dummy_app/app/active_record/paper_trail_test/subclass_in_namespace.rb index 15d8d28403..a50aaaebc5 100644 --- a/spec/dummy_app/app/active_record/paper_trail_test/subclass_in_namespace.rb +++ b/spec/dummy_app/app/active_record/paper_trail_test/subclass_in_namespace.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PaperTrailTest < ActiveRecord::Base class SubclassInNamespace < self end diff --git a/spec/dummy_app/app/active_record/paper_trail_test_subclass.rb b/spec/dummy_app/app/active_record/paper_trail_test_subclass.rb index 790dba3d9d..e4feca1d0f 100644 --- a/spec/dummy_app/app/active_record/paper_trail_test_subclass.rb +++ b/spec/dummy_app/app/active_record/paper_trail_test_subclass.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class PaperTrailTestSubclass < PaperTrailTest end diff --git a/spec/dummy_app/app/active_record/paper_trail_test_with_custom_association.rb b/spec/dummy_app/app/active_record/paper_trail_test_with_custom_association.rb new file mode 100644 index 0000000000..f339135b11 --- /dev/null +++ b/spec/dummy_app/app/active_record/paper_trail_test_with_custom_association.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class PaperTrailTestWithCustomAssociation < ActiveRecord::Base + self.table_name = :paper_trail_tests + has_paper_trail versions: {class_name: 'Trail'} +end diff --git a/spec/dummy_app/app/active_record/player.rb b/spec/dummy_app/app/active_record/player.rb index e73dab2d49..e5024b93f7 100644 --- a/spec/dummy_app/app/active_record/player.rb +++ b/spec/dummy_app/app/active_record/player.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Player < ActiveRecord::Base belongs_to :team, optional: true, inverse_of: :players has_one :draft, dependent: :destroy @@ -7,26 +9,18 @@ class Player < ActiveRecord::Base validates_numericality_of(:number, only_integer: true) validates_uniqueness_of(:number, scope: :team_id, message: 'There is already a player with that number on this team') validates_each :name do |record, _attr, value| - record.errors.add(:base, 'Player is cheating') if value.to_s =~ /on steroids/ + record.errors.add(:base, 'Player is cheating') if /on steroids/.match?(value.to_s) end - enum formation: {start: 'start', substitute: 'substitute'} + if ActiveRecord.gem_version >= Gem::Version.new('7.0') + enum :formation, {start: 'start', substitute: 'substitute'} + else + enum formation: {start: 'start', substitute: 'substitute'} + end before_destroy :destroy_hook scope :rails_admin_search, ->(query) { where(name: query.reverse) } def destroy_hook; end - - def draft_id - draft.try :id - end - - def draft_id=(id) - self.draft = Draft.find_by_id(id) - end - - def number_name - "#{number} #{name}" - end end diff --git a/spec/dummy_app/app/active_record/read_only_comment.rb b/spec/dummy_app/app/active_record/read_only_comment.rb new file mode 100644 index 0000000000..7188c58bf4 --- /dev/null +++ b/spec/dummy_app/app/active_record/read_only_comment.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ReadOnlyComment < Comment + def readonly? + true + end +end diff --git a/spec/dummy_app/app/active_record/restricted_team.rb b/spec/dummy_app/app/active_record/restricted_team.rb new file mode 100644 index 0000000000..aded1a6446 --- /dev/null +++ b/spec/dummy_app/app/active_record/restricted_team.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class RestrictedTeam < Team + has_many :players, foreign_key: :team_id, dependent: :restrict_with_error +end diff --git a/spec/dummy_app/app/active_record/shrine_uploader.rb b/spec/dummy_app/app/active_record/shrine_uploader.rb index b8ab3ba0d8..aef29cc30e 100644 --- a/spec/dummy_app/app/active_record/shrine_uploader.rb +++ b/spec/dummy_app/app/active_record/shrine_uploader.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ShrineUploader < Shrine plugin :activerecord diff --git a/spec/dummy_app/app/active_record/shrine_versioning_uploader.rb b/spec/dummy_app/app/active_record/shrine_versioning_uploader.rb index 96c5d7d21b..b9e5e3d50e 100644 --- a/spec/dummy_app/app/active_record/shrine_versioning_uploader.rb +++ b/spec/dummy_app/app/active_record/shrine_versioning_uploader.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ShrineVersioningUploader < Shrine plugin :activerecord diff --git a/spec/dummy_app/app/active_record/team.rb b/spec/dummy_app/app/active_record/team.rb index 9cb7b06970..dd2c2dde30 100644 --- a/spec/dummy_app/app/active_record/team.rb +++ b/spec/dummy_app/app/active_record/team.rb @@ -1,4 +1,4 @@ -# coding: utf-8 +# frozen_string_literal: true class Team < ActiveRecord::Base has_many :players, -> { order :id }, inverse_of: :team @@ -14,7 +14,11 @@ class Team < ActiveRecord::Base validates_numericality_of :revenue, allow_nil: true belongs_to :division, optional: true - enum main_sponsor: [:no_sponsor, :food_factory, :transportation_company, :bank, :energy_producer] + if ActiveRecord.gem_version >= Gem::Version.new('7.0') + enum :main_sponsor, %i[no_sponsor food_factory transportation_company bank energy_producer] + else + enum main_sponsor: %i[no_sponsor food_factory transportation_company bank energy_producer] + end def player_names_truncated players.collect(&:name).join(', ')[0..32] @@ -27,4 +31,8 @@ def color_enum scope :green, -> { where(color: 'red') } scope :red, -> { where(color: 'red') } scope :white, -> { where(color: 'white') } + + rails_admin do + field :color, :color + end end diff --git a/spec/dummy_app/app/active_record/trail.rb b/spec/dummy_app/app/active_record/trail.rb new file mode 100644 index 0000000000..87ccdb7bb5 --- /dev/null +++ b/spec/dummy_app/app/active_record/trail.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Trail < PaperTrail::Version + self.table_name = :custom_versions +end diff --git a/spec/dummy_app/app/active_record/two_level/namespaced.rb b/spec/dummy_app/app/active_record/two_level/namespaced.rb new file mode 100644 index 0000000000..4b96436af5 --- /dev/null +++ b/spec/dummy_app/app/active_record/two_level/namespaced.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module TwoLevel + module Namespaced + def self.table_name_prefix + 'two_level_namespaced_' + end + end +end diff --git a/spec/dummy_app/app/active_record/two_level/namespaced/polymorphic_association_test.rb b/spec/dummy_app/app/active_record/two_level/namespaced/polymorphic_association_test.rb new file mode 100644 index 0000000000..45229ae966 --- /dev/null +++ b/spec/dummy_app/app/active_record/two_level/namespaced/polymorphic_association_test.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module TwoLevel + module Namespaced + class PolymorphicAssociationTest < ActiveRecord::Base + has_many :comments, as: :commentable + end + end +end diff --git a/spec/dummy_app/app/active_record/user.rb b/spec/dummy_app/app/active_record/user.rb index 1d70ff878a..b091038a86 100644 --- a/spec/dummy_app/app/active_record/user.rb +++ b/spec/dummy_app/app/active_record/user.rb @@ -1,14 +1,21 @@ +# frozen_string_literal: true + class User < ActiveRecord::Base # Include default devise modules. Others available are: # :token_authenticatable, :confirmable, :lockable and :timeoutable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable - serialize :roles, Array + if ActiveRecord.gem_version < Gem::Version.new('7.1') + serialize :roles, Array + else + serialize :roles, coder: YAML, type: Array + end # Add Paperclip support for avatars has_attached_file :avatar, styles: {medium: '300x300>', thumb: '100x100>'} attr_accessor :delete_avatar + before_validation { self.avatar = nil if delete_avatar == '1' } def attr_accessible_role @@ -16,6 +23,6 @@ def attr_accessible_role end def roles_enum - [:admin, :user] + %i[admin user] end end diff --git a/spec/dummy_app/app/active_record/user/confirmed.rb b/spec/dummy_app/app/active_record/user/confirmed.rb index 40aabe12c4..5c3abbbc66 100644 --- a/spec/dummy_app/app/active_record/user/confirmed.rb +++ b/spec/dummy_app/app/active_record/user/confirmed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class User class Confirmed < User end diff --git a/spec/dummy_app/app/active_record/without_table.rb b/spec/dummy_app/app/active_record/without_table.rb index da5a23a207..f74f82ede5 100644 --- a/spec/dummy_app/app/active_record/without_table.rb +++ b/spec/dummy_app/app/active_record/without_table.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class WithoutTable < ActiveRecord::Base end diff --git a/app/assets/stylesheets/rails_admin/themes/cerulean/mixins.scss b/spec/dummy_app/app/assets/builds/.keep similarity index 100% rename from app/assets/stylesheets/rails_admin/themes/cerulean/mixins.scss rename to spec/dummy_app/app/assets/builds/.keep diff --git a/spec/dummy_app/app/assets/config/manifest.js b/spec/dummy_app/app/assets/config/manifest.js index 5cc2c08940..04081559e3 100644 --- a/spec/dummy_app/app/assets/config/manifest.js +++ b/spec/dummy_app/app/assets/config/manifest.js @@ -1,3 +1,4 @@ //= link_tree ../images -//= link_directory ../javascripts .js -//= link_directory ../stylesheets .css \ No newline at end of file +//= link_tree ../../../../../src .js +//= link application.js +//= link application.css diff --git a/spec/dummy_app/app/assets/javascripts/application.js b/spec/dummy_app/app/assets/javascripts/application.js index 9097d830e2..e844b587d7 100644 --- a/spec/dummy_app/app/assets/javascripts/application.js +++ b/spec/dummy_app/app/assets/javascripts/application.js @@ -10,6 +10,5 @@ // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD // GO AFTER THE REQUIRES BELOW. // -//= require jquery -//= require jquery_ujs -//= require_tree . +//= require rails-ujs.esm +//= require turbo diff --git a/spec/dummy_app/app/assets/javascripts/rails-ujs.esm.js.erb b/spec/dummy_app/app/assets/javascripts/rails-ujs.esm.js.erb new file mode 100644 index 0000000000..f05a98c02b --- /dev/null +++ b/spec/dummy_app/app/assets/javascripts/rails-ujs.esm.js.erb @@ -0,0 +1 @@ +<%= depend_on_asset('rails-ujs').to_s.gsub(/context = this/, 'context = (this || globalThis)') %> \ No newline at end of file diff --git a/spec/dummy_app/app/assets/javascripts/rails_admin/custom/ui.js b/spec/dummy_app/app/assets/javascripts/rails_admin/custom/ui.js new file mode 100644 index 0000000000..44506531d1 --- /dev/null +++ b/spec/dummy_app/app/assets/javascripts/rails_admin/custom/ui.js @@ -0,0 +1,9 @@ +window.domReadyTriggered = []; + +document.addEventListener("rails_admin.dom_ready", function () { + window.domReadyTriggered.push("plainjs/dot"); +}); + +$(document).on("rails_admin.dom_ready", function () { + window.domReadyTriggered.push("jquery/dot"); +}); diff --git a/spec/dummy_app/app/assets/stylesheets/application.css b/spec/dummy_app/app/assets/stylesheets/application.css index 3b5cc6648e..960f2c08fe 100644 --- a/spec/dummy_app/app/assets/stylesheets/application.css +++ b/spec/dummy_app/app/assets/stylesheets/application.css @@ -9,5 +9,4 @@ * compiled file, but it's generally better to create a new file per style scope. * *= require_self - *= require_tree . */ diff --git a/spec/dummy_app/app/assets/stylesheets/rails_admin.scss b/spec/dummy_app/app/assets/stylesheets/rails_admin.scss new file mode 100644 index 0000000000..3c6aad725b --- /dev/null +++ b/spec/dummy_app/app/assets/stylesheets/rails_admin.scss @@ -0,0 +1,3 @@ +$fa-font-path: "."; +@import "rails_admin/src/rails_admin/styles/base.scss"; +@import "trix/dist/trix"; diff --git a/spec/dummy_app/app/assets/stylesheets/rails_admin/custom/theming.scss b/spec/dummy_app/app/assets/stylesheets/rails_admin/custom/theming.scss new file mode 100644 index 0000000000..6c271d1c6e --- /dev/null +++ b/spec/dummy_app/app/assets/stylesheets/rails_admin/custom/theming.scss @@ -0,0 +1,3 @@ +.navbar-brand small { + opacity: 0.99; +} diff --git a/spec/dummy_app/app/controllers/application_controller.rb b/spec/dummy_app/app/controllers/application_controller.rb index e8065d9505..9c4e35bac3 100644 --- a/spec/dummy_app/app/controllers/application_controller.rb +++ b/spec/dummy_app/app/controllers/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationController < ActionController::Base protect_from_forgery end diff --git a/spec/dummy_app/app/controllers/players_controller.rb b/spec/dummy_app/app/controllers/players_controller.rb index 2831ab5962..e1716fd265 100644 --- a/spec/dummy_app/app/controllers/players_controller.rb +++ b/spec/dummy_app/app/controllers/players_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PlayersController < ApplicationController def show @player = Player.find(params[:id]) diff --git a/spec/dummy_app/app/eager_loaded/basketball.rb b/spec/dummy_app/app/eager_loaded/basketball.rb new file mode 100644 index 0000000000..28dbe974d4 --- /dev/null +++ b/spec/dummy_app/app/eager_loaded/basketball.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class Basketball < Ball +end diff --git a/spec/dummy_app/app/frontend/entrypoints/application.js b/spec/dummy_app/app/frontend/entrypoints/application.js new file mode 100644 index 0000000000..7bb62d5763 --- /dev/null +++ b/spec/dummy_app/app/frontend/entrypoints/application.js @@ -0,0 +1,2 @@ +import "@rails/ujs"; +import "@hotwired/turbo-rails"; diff --git a/spec/dummy_app/app/frontend/entrypoints/rails_admin.js b/spec/dummy_app/app/frontend/entrypoints/rails_admin.js new file mode 100644 index 0000000000..08b5caf6e6 --- /dev/null +++ b/spec/dummy_app/app/frontend/entrypoints/rails_admin.js @@ -0,0 +1,17 @@ +import "~/stylesheets/rails_admin.scss"; +import "rails_admin/src/rails_admin/base"; +import "flatpickr/dist/l10n/fr.js"; +import "trix"; +import "@rails/actiontext"; +import * as ActiveStorage from "@rails/activestorage"; +ActiveStorage.start(); + +window.domReadyTriggered = []; + +document.addEventListener("rails_admin.dom_ready", function () { + window.domReadyTriggered.push("plainjs/dot"); +}); + +$(document).on("rails_admin.dom_ready", function () { + window.domReadyTriggered.push("jquery/dot"); +}); diff --git a/spec/dummy_app/app/frontend/stylesheets/rails_admin.scss b/spec/dummy_app/app/frontend/stylesheets/rails_admin.scss new file mode 100644 index 0000000000..eb98071c79 --- /dev/null +++ b/spec/dummy_app/app/frontend/stylesheets/rails_admin.scss @@ -0,0 +1,2 @@ +$fa-font-path: "@fortawesome/fontawesome-free/webfonts"; +@import "rails_admin/src/rails_admin/styles/base"; diff --git a/spec/dummy_app/app/javascript/application.js b/spec/dummy_app/app/javascript/application.js new file mode 100644 index 0000000000..6100af1565 --- /dev/null +++ b/spec/dummy_app/app/javascript/application.js @@ -0,0 +1,4 @@ +import Rails from "@rails/ujs"; +import "@hotwired/turbo-rails"; + +Rails.start(); diff --git a/spec/dummy_app/app/javascript/rails_admin.js b/spec/dummy_app/app/javascript/rails_admin.js new file mode 100644 index 0000000000..038a150f86 --- /dev/null +++ b/spec/dummy_app/app/javascript/rails_admin.js @@ -0,0 +1,16 @@ +import "rails_admin/src/rails_admin/base"; +import "flatpickr/dist/l10n/fr.js"; +import "trix"; +import "@rails/actiontext"; +import * as ActiveStorage from "@rails/activestorage"; +ActiveStorage.start(); + +window.domReadyTriggered = []; + +document.addEventListener("rails_admin.dom_ready", function () { + window.domReadyTriggered.push("plainjs/dot"); +}); + +$(document).on("rails_admin.dom_ready", function () { + window.domReadyTriggered.push("jquery/dot"); +}); diff --git a/spec/dummy_app/app/javascript/rails_admin.scss b/spec/dummy_app/app/javascript/rails_admin.scss new file mode 100644 index 0000000000..3a122e60eb --- /dev/null +++ b/spec/dummy_app/app/javascript/rails_admin.scss @@ -0,0 +1 @@ +@import "rails_admin/src/rails_admin/styles/base.scss"; diff --git a/spec/dummy_app/app/jobs/application_job.rb b/spec/dummy_app/app/jobs/application_job.rb new file mode 100644 index 0000000000..bef395997d --- /dev/null +++ b/spec/dummy_app/app/jobs/application_job.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/spec/dummy_app/app/jobs/null_job.rb b/spec/dummy_app/app/jobs/null_job.rb new file mode 100644 index 0000000000..6ca7297312 --- /dev/null +++ b/spec/dummy_app/app/jobs/null_job.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class NullJob < ApplicationJob + queue_as :default + + def perform(*args) + # Do nothing + end +end diff --git a/spec/dummy_app/app/locales/models.en.yml b/spec/dummy_app/app/locales/models.en.yml index 24d042ebd6..44f0b98e7b 100644 --- a/spec/dummy_app/app/locales/models.en.yml +++ b/spec/dummy_app/app/locales/models.en.yml @@ -7,8 +7,7 @@ en: manager: Team Manager main_sponsor: Main Sponsor fans: Some Fans - mongoid: - *en_attributes + mongoid: *en_attributes fr: activerecord: &fr_attributes attributes: @@ -17,5 +16,4 @@ fr: team: manager: Manager de l'équipe fans: Quelques fans - mongoid: - *fr_attributes + mongoid: *fr_attributes diff --git a/spec/dummy_app/app/mongoid/another_field_test.rb b/spec/dummy_app/app/mongoid/another_field_test.rb index 8df69dcbff..402c1ad2c7 100644 --- a/spec/dummy_app/app/mongoid/another_field_test.rb +++ b/spec/dummy_app/app/mongoid/another_field_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AnotherFieldTest include Mongoid::Document diff --git a/spec/dummy_app/app/mongoid/ball.rb b/spec/dummy_app/app/mongoid/ball.rb index b862e5dd66..f641754a16 100644 --- a/spec/dummy_app/app/mongoid/ball.rb +++ b/spec/dummy_app/app/mongoid/ball.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Ball include Mongoid::Document include Mongoid::Timestamps diff --git a/spec/dummy_app/app/mongoid/carrierwave_uploader.rb b/spec/dummy_app/app/mongoid/carrierwave_uploader.rb index 3fadff5ce4..e4b50dbbd0 100644 --- a/spec/dummy_app/app/mongoid/carrierwave_uploader.rb +++ b/spec/dummy_app/app/mongoid/carrierwave_uploader.rb @@ -1,4 +1,5 @@ -# encoding: utf-8 +# frozen_string_literal: true + require 'mini_magick' class CarrierwaveUploader < CarrierWave::Uploader::Base # Include RMagick or ImageScience support: @@ -37,9 +38,9 @@ def store_dir # process scale: [50, 50] # end - # Add a white list of extensions which are allowed to be uploaded. + # Add an allowlist of extensions which are allowed to be uploaded. # For images you might use something like this: - # def extension_white_list + # def extension_allowlist # %w(jpg jpeg gif png) # end diff --git a/spec/dummy_app/app/mongoid/category.rb b/spec/dummy_app/app/mongoid/category.rb index 46a8a08bfc..b5a4c8b355 100644 --- a/spec/dummy_app/app/mongoid/category.rb +++ b/spec/dummy_app/app/mongoid/category.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Category include Mongoid::Document diff --git a/spec/dummy_app/app/mongoid/cms.rb b/spec/dummy_app/app/mongoid/cms.rb index a868656d3e..bb97036cf3 100644 --- a/spec/dummy_app/app/mongoid/cms.rb +++ b/spec/dummy_app/app/mongoid/cms.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Cms def self.table_name_prefix 'cms_' diff --git a/spec/dummy_app/app/mongoid/cms/basic_page.rb b/spec/dummy_app/app/mongoid/cms/basic_page.rb index 77ec23a86f..5d82bc0975 100644 --- a/spec/dummy_app/app/mongoid/cms/basic_page.rb +++ b/spec/dummy_app/app/mongoid/cms/basic_page.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Cms class BasicPage include Mongoid::Document diff --git a/spec/dummy_app/app/mongoid/comment.rb b/spec/dummy_app/app/mongoid/comment.rb index 453d4470b0..9f906d9b55 100644 --- a/spec/dummy_app/app/mongoid/comment.rb +++ b/spec/dummy_app/app/mongoid/comment.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Comment include Mongoid::Document field :content, type: String diff --git a/spec/dummy_app/app/mongoid/comment/confirmed.rb b/spec/dummy_app/app/mongoid/comment/confirmed.rb index abbf700ca2..cf8fa4221b 100644 --- a/spec/dummy_app/app/mongoid/comment/confirmed.rb +++ b/spec/dummy_app/app/mongoid/comment/confirmed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Comment class Confirmed < Comment default_scope -> { where(content: 'something') } diff --git a/spec/dummy_app/app/mongoid/concerns/taggable.rb b/spec/dummy_app/app/mongoid/concerns/taggable.rb index be7c776ece..6293ab069b 100644 --- a/spec/dummy_app/app/mongoid/concerns/taggable.rb +++ b/spec/dummy_app/app/mongoid/concerns/taggable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Taggable extend ActiveSupport::Concern # dummy diff --git a/spec/dummy_app/app/mongoid/deeply_nested_field_test.rb b/spec/dummy_app/app/mongoid/deeply_nested_field_test.rb index c61131ca54..ab352d8ca2 100644 --- a/spec/dummy_app/app/mongoid/deeply_nested_field_test.rb +++ b/spec/dummy_app/app/mongoid/deeply_nested_field_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DeeplyNestedFieldTest include Mongoid::Document include Mongoid::Timestamps diff --git a/spec/dummy_app/app/mongoid/division.rb b/spec/dummy_app/app/mongoid/division.rb index 75edd6bfd6..73ded46ec9 100644 --- a/spec/dummy_app/app/mongoid/division.rb +++ b/spec/dummy_app/app/mongoid/division.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Division include Mongoid::Document include Mongoid::Timestamps diff --git a/spec/dummy_app/app/mongoid/draft.rb b/spec/dummy_app/app/mongoid/draft.rb index ff32ea1362..86f8c465ab 100644 --- a/spec/dummy_app/app/mongoid/draft.rb +++ b/spec/dummy_app/app/mongoid/draft.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Draft include Mongoid::Document include Mongoid::Timestamps diff --git a/spec/dummy_app/app/mongoid/embed.rb b/spec/dummy_app/app/mongoid/embed.rb index f297ae4254..bc015cba36 100644 --- a/spec/dummy_app/app/mongoid/embed.rb +++ b/spec/dummy_app/app/mongoid/embed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Embed include Mongoid::Document field :name, type: String diff --git a/spec/dummy_app/app/mongoid/fan.rb b/spec/dummy_app/app/mongoid/fan.rb index 0e52ea76e2..26e871ae86 100644 --- a/spec/dummy_app/app/mongoid/fan.rb +++ b/spec/dummy_app/app/mongoid/fan.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Fan include Mongoid::Document include Mongoid::Timestamps diff --git a/spec/dummy_app/app/mongoid/field_test.rb b/spec/dummy_app/app/mongoid/field_test.rb index 427db8a9a3..57834e2ee5 100644 --- a/spec/dummy_app/app/mongoid/field_test.rb +++ b/spec/dummy_app/app/mongoid/field_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_admin/adapters/mongoid' class FieldTest @@ -31,8 +33,10 @@ class FieldTest field :time_field, type: Time field :format, type: String + field :open, type: Boolean field :restricted_field, type: String field :protected_field, type: String + field :al, as: :aliased_field, type: String has_mongoid_attached_file :paperclip_asset, styles: {thumb: '100x100>'} field :shrine_asset_data, type: String @@ -51,6 +55,7 @@ class FieldTest accepts_nested_attributes_for :embeds, allow_destroy: true attr_accessor :delete_paperclip_asset + before_validation { self.paperclip_asset = nil if delete_paperclip_asset == '1' } field :dragonfly_asset_name @@ -60,7 +65,7 @@ class FieldTest mount_uploader :carrierwave_asset, CarrierwaveUploader # carrierwave-mongoid does not support mount_uploaders yet: # https://github.com/carrierwaveuploader/carrierwave-mongoid/issues/138 - mount_uploader :carrierwave_assets, CarrierwaveUploader + mount_uploaders :carrierwave_assets, CarrierwaveUploader validates :short_text, length: {maximum: 255} end diff --git a/spec/dummy_app/app/mongoid/hardball.rb b/spec/dummy_app/app/mongoid/hardball.rb index 3f7b98da4b..7de1a351db 100644 --- a/spec/dummy_app/app/mongoid/hardball.rb +++ b/spec/dummy_app/app/mongoid/hardball.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class Hardball < Ball end diff --git a/spec/dummy_app/app/mongoid/image.rb b/spec/dummy_app/app/mongoid/image.rb index 86dfe646f6..5505322053 100644 --- a/spec/dummy_app/app/mongoid/image.rb +++ b/spec/dummy_app/app/mongoid/image.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Image include Mongoid::Document include Mongoid::Paperclip diff --git a/spec/dummy_app/app/mongoid/league.rb b/spec/dummy_app/app/mongoid/league.rb index c7153a6992..ecb00150eb 100644 --- a/spec/dummy_app/app/mongoid/league.rb +++ b/spec/dummy_app/app/mongoid/league.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class League include Mongoid::Document diff --git a/spec/dummy_app/app/mongoid/managed_team.rb b/spec/dummy_app/app/mongoid/managed_team.rb index 4b6488282e..3b0894ad58 100644 --- a/spec/dummy_app/app/mongoid/managed_team.rb +++ b/spec/dummy_app/app/mongoid/managed_team.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ManagedTeam < Team belongs_to :user, class_name: 'ManagingUser', foreign_key: :manager, primary_key: :email, inverse_of: :teams end diff --git a/spec/dummy_app/app/mongoid/managing_user.rb b/spec/dummy_app/app/mongoid/managing_user.rb index 9bba8f3d79..010d9862c8 100644 --- a/spec/dummy_app/app/mongoid/managing_user.rb +++ b/spec/dummy_app/app/mongoid/managing_user.rb @@ -1,8 +1,8 @@ +# frozen_string_literal: true + class ManagingUser < User has_one :team, class_name: 'ManagedTeam', foreign_key: :manager, primary_key: :email, inverse_of: :user has_many :teams, class_name: 'ManagedTeam', foreign_key: :manager, primary_key: :email, inverse_of: :user - - def team_id=(id) - self.team = ManagedTeam.where(_id: id).first - end + has_and_belongs_to_many :players, foreign_key: :player_names, primary_key: :name, inverse_of: :nil + has_and_belongs_to_many :balls, primary_key: :color, inverse_of: :nil end diff --git a/spec/dummy_app/app/mongoid/nested_field_test.rb b/spec/dummy_app/app/mongoid/nested_field_test.rb index 1315a98796..8d2200dd23 100644 --- a/spec/dummy_app/app/mongoid/nested_field_test.rb +++ b/spec/dummy_app/app/mongoid/nested_field_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class NestedFieldTest include Mongoid::Document diff --git a/spec/dummy_app/app/mongoid/player.rb b/spec/dummy_app/app/mongoid/player.rb index 618014139b..49baee2feb 100644 --- a/spec/dummy_app/app/mongoid/player.rb +++ b/spec/dummy_app/app/mongoid/player.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Player include Mongoid::Document include Mongoid::Timestamps @@ -20,7 +22,7 @@ class Player validates_uniqueness_of(:number, scope: :team_id, message: 'There is already a player with that number on this team') validates_each :name do |record, _attr, value| - record.errors.add(:base, 'Player is cheating') if value.to_s =~ /on steroids/ + record.errors.add(:base, 'Player is cheating') if /on steroids/.match?(value.to_s) end has_one :draft, dependent: :destroy @@ -31,12 +33,4 @@ class Player scope :rails_admin_search, ->(query) { where(name: query.reverse) } def destroy_hook; end - - def draft_id - draft.try :id - end - - def draft_id=(id) - self.draft = Draft.where(_id: id).first - end end diff --git a/spec/dummy_app/app/mongoid/read_only_comment.rb b/spec/dummy_app/app/mongoid/read_only_comment.rb new file mode 100644 index 0000000000..7188c58bf4 --- /dev/null +++ b/spec/dummy_app/app/mongoid/read_only_comment.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ReadOnlyComment < Comment + def readonly? + true + end +end diff --git a/spec/dummy_app/app/mongoid/restricted_team.rb b/spec/dummy_app/app/mongoid/restricted_team.rb new file mode 100644 index 0000000000..aded1a6446 --- /dev/null +++ b/spec/dummy_app/app/mongoid/restricted_team.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class RestrictedTeam < Team + has_many :players, foreign_key: :team_id, dependent: :restrict_with_error +end diff --git a/spec/dummy_app/app/mongoid/shrine_uploader.rb b/spec/dummy_app/app/mongoid/shrine_uploader.rb index c9cb9533ce..982f75d2fc 100644 --- a/spec/dummy_app/app/mongoid/shrine_uploader.rb +++ b/spec/dummy_app/app/mongoid/shrine_uploader.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ShrineUploader < Shrine plugin :mongoid diff --git a/spec/dummy_app/app/mongoid/shrine_versioning_uploader.rb b/spec/dummy_app/app/mongoid/shrine_versioning_uploader.rb index d787453c37..a50aa94cf0 100644 --- a/spec/dummy_app/app/mongoid/shrine_versioning_uploader.rb +++ b/spec/dummy_app/app/mongoid/shrine_versioning_uploader.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ShrineVersioningUploader < Shrine plugin :mongoid diff --git a/spec/dummy_app/app/mongoid/team.rb b/spec/dummy_app/app/mongoid/team.rb index 48acd1f571..190c567d14 100644 --- a/spec/dummy_app/app/mongoid/team.rb +++ b/spec/dummy_app/app/mongoid/team.rb @@ -1,4 +1,4 @@ -# coding: utf-8 +# frozen_string_literal: true class Team include Mongoid::Document diff --git a/spec/dummy_app/app/mongoid/two_level/namespaced/polymorphic_association_test.rb b/spec/dummy_app/app/mongoid/two_level/namespaced/polymorphic_association_test.rb new file mode 100644 index 0000000000..d8f01b733d --- /dev/null +++ b/spec/dummy_app/app/mongoid/two_level/namespaced/polymorphic_association_test.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module TwoLevel + module Namespaced + class PolymorphicAssociationTest + include Mongoid::Document + + field :name, type: String + + has_many :comments, as: :commentable + end + end +end diff --git a/spec/dummy_app/app/mongoid/user.rb b/spec/dummy_app/app/mongoid/user.rb index 676cd2373a..788440e1cf 100644 --- a/spec/dummy_app/app/mongoid/user.rb +++ b/spec/dummy_app/app/mongoid/user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class User include Mongoid::Document include Mongoid::Paperclip @@ -56,5 +58,6 @@ def attr_accessible_role end attr_accessor :delete_avatar + before_validation { self.avatar = nil if delete_avatar == '1' } end diff --git a/spec/dummy_app/app/mongoid/user/confirmed.rb b/spec/dummy_app/app/mongoid/user/confirmed.rb index 40aabe12c4..5c3abbbc66 100644 --- a/spec/dummy_app/app/mongoid/user/confirmed.rb +++ b/spec/dummy_app/app/mongoid/user/confirmed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class User class Confirmed < User end diff --git a/spec/dummy_app/app/views/layouts/application.html.erb b/spec/dummy_app/app/views/layouts/application.html.erb index 6f5cc0446b..680c846ac5 100644 --- a/spec/dummy_app/app/views/layouts/application.html.erb +++ b/spec/dummy_app/app/views/layouts/application.html.erb @@ -2,8 +2,18 @@ DummyApp - <%= stylesheet_link_tag "application", media: "all" %> - <%= javascript_include_tag "application" %> + <% case CI_ASSET %> + <% when :webpacker %> + <%= javascript_pack_tag "application" %> + <% when :importmap %> + <%= javascript_importmap_tags %> + <% when :vite %> + <%= vite_client_tag %> + <%= vite_javascript_tag 'application' %> + <% else %> + <%= stylesheet_link_tag "application", media: "all" %> + <%= javascript_include_tag "application", type: 'module' %> + <% end %> <%= csrf_meta_tags %> diff --git a/spec/dummy_app/app/views/players/show.html.erb b/spec/dummy_app/app/views/players/show.html.erb new file mode 100644 index 0000000000..0174e68082 --- /dev/null +++ b/spec/dummy_app/app/views/players/show.html.erb @@ -0,0 +1,2 @@ +

    <%= @player.name %>

    +<%= link_to 'Back to admin', rails_admin.show_path(model_name: 'player', id: @player.id) %> \ No newline at end of file diff --git a/spec/dummy_app/app/views/players/show.html.haml b/spec/dummy_app/app/views/players/show.html.haml deleted file mode 100644 index 99dc395d57..0000000000 --- a/spec/dummy_app/app/views/players/show.html.haml +++ /dev/null @@ -1 +0,0 @@ -%h1=@player.name diff --git a/spec/dummy_app/babel.config.js b/spec/dummy_app/babel.config.js new file mode 100644 index 0000000000..903ded5e36 --- /dev/null +++ b/spec/dummy_app/babel.config.js @@ -0,0 +1,82 @@ +module.exports = function (api) { + var validEnv = ["development", "test", "production"]; + var currentEnv = api.env(); + var isDevelopmentEnv = api.env("development"); + var isProductionEnv = api.env("production"); + var isTestEnv = api.env("test"); + + if (!validEnv.includes(currentEnv)) { + throw new Error( + "Please specify a valid `NODE_ENV` or " + + '`BABEL_ENV` environment variables. Valid values are "development", ' + + '"test", and "production". Instead, received: ' + + JSON.stringify(currentEnv) + + "." + ); + } + + return { + presets: [ + isTestEnv && [ + "@babel/preset-env", + { + targets: { + node: "current", + }, + }, + ], + (isProductionEnv || isDevelopmentEnv) && [ + "@babel/preset-env", + { + forceAllTransforms: true, + useBuiltIns: "entry", + corejs: 3, + modules: false, + exclude: ["transform-typeof-symbol"], + }, + ], + ].filter(Boolean), + plugins: [ + "babel-plugin-macros", + "@babel/plugin-syntax-dynamic-import", + isTestEnv && "babel-plugin-dynamic-import-node", + "@babel/plugin-transform-destructuring", + [ + "@babel/plugin-proposal-class-properties", + { + loose: true, + }, + ], + [ + "@babel/plugin-proposal-object-rest-spread", + { + useBuiltIns: true, + }, + ], + [ + "@babel/plugin-proposal-private-methods", + { + loose: true, + }, + ], + [ + "@babel/plugin-proposal-private-property-in-object", + { + loose: true, + }, + ], + [ + "@babel/plugin-transform-runtime", + { + helpers: false, + }, + ], + [ + "@babel/plugin-transform-regenerator", + { + async: false, + }, + ], + ].filter(Boolean), + }; +}; diff --git a/spec/dummy_app/bin/bundle b/spec/dummy_app/bin/bundle index 66e9889e8b..981e650b68 100755 --- a/spec/dummy_app/bin/bundle +++ b/spec/dummy_app/bin/bundle @@ -1,3 +1,114 @@ #!/usr/bin/env ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -load Gem.bin_path('bundler', 'bundle') +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + requirement = bundler_gem_version.approximate_recommendation + + return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0") + + requirement += ".a" if bundler_gem_version.prerelease? + + requirement + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/spec/dummy_app/bin/dev b/spec/dummy_app/bin/dev new file mode 100755 index 0000000000..a1104a50b1 --- /dev/null +++ b/spec/dummy_app/bin/dev @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +if ! command -v foreman &> /dev/null +then + echo "Installing foreman..." + gem install foreman +fi + +foreman start -f Procfile.dev "$@" diff --git a/spec/dummy_app/bin/docker-entrypoint b/spec/dummy_app/bin/docker-entrypoint new file mode 100755 index 0000000000..183c9fa576 --- /dev/null +++ b/spec/dummy_app/bin/docker-entrypoint @@ -0,0 +1,8 @@ +#!/bin/bash -e + +# If running the rails server then create or migrate existing database +if [ "${*}" == "./bin/rails server" ]; then + ./bin/rails db:create db:migrate db:seed +fi + +exec "${@}" diff --git a/spec/dummy_app/bin/importmap b/spec/dummy_app/bin/importmap new file mode 100755 index 0000000000..36502ab16c --- /dev/null +++ b/spec/dummy_app/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/spec/dummy_app/bin/vite b/spec/dummy_app/bin/vite new file mode 100755 index 0000000000..7527d097eb --- /dev/null +++ b/spec/dummy_app/bin/vite @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'vite' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("vite_ruby", "vite") diff --git a/spec/dummy_app/bin/webpack b/spec/dummy_app/bin/webpack new file mode 100755 index 0000000000..1031168d01 --- /dev/null +++ b/spec/dummy_app/bin/webpack @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" +ENV["NODE_ENV"] ||= "development" + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "bundler/setup" + +require "webpacker" +require "webpacker/webpack_runner" + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + Webpacker::WebpackRunner.run(ARGV) +end diff --git a/spec/dummy_app/bin/webpack-dev-server b/spec/dummy_app/bin/webpack-dev-server new file mode 100755 index 0000000000..dd9662737a --- /dev/null +++ b/spec/dummy_app/bin/webpack-dev-server @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" +ENV["NODE_ENV"] ||= "development" + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "bundler/setup" + +require "webpacker" +require "webpacker/dev_server_runner" + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + Webpacker::DevServerRunner.run(ARGV) +end diff --git a/spec/dummy_app/config.ru b/spec/dummy_app/config.ru index 7ae1c84900..a82a2446e9 100644 --- a/spec/dummy_app/config.ru +++ b/spec/dummy_app/config.ru @@ -1,4 +1,6 @@ +# frozen_string_literal: true + # This file is used by Rack-based servers to start the application. -require ::File.expand_path('../config/environment', __FILE__) +require ::File.expand_path('config/environment', __dir__) run DummyApp::Application diff --git a/spec/dummy_app/config/application.rb b/spec/dummy_app/config/application.rb index 0f07c37b86..c07817cf9f 100644 --- a/spec/dummy_app/config/application.rb +++ b/spec/dummy_app/config/application.rb @@ -1,17 +1,31 @@ -require File.expand_path('../boot', __FILE__) +# frozen_string_literal: true + +require File.expand_path('boot', __dir__) require 'action_controller/railtie' require 'action_mailer/railtie' -require 'sprockets/railtie' begin require CI_ORM.to_s require "#{CI_ORM}/railtie" -rescue LoadError # rubocop:disable Lint/HandleExceptions +rescue LoadError + # ignore errors end -require 'active_storage/engine' if Rails.version >= '5.2.0' && CI_ORM == :active_record -require 'action_text/engine' if Rails.version >= '6.0.0' && CI_ORM == :active_record +require 'active_storage/engine' if CI_ORM == :active_record +require 'action_text/engine' if CI_ORM == :active_record + +case CI_ASSET +when :webpacker + require 'webpacker' +when :sprockets, :webpack + require 'sprockets/railtie' +when :importmap + require 'sprockets/railtie' + require 'importmap-rails' +when :vite + require 'vite_rails' +end # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. @@ -22,12 +36,34 @@ class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. - config.load_defaults Rails.version[0, 3] if Rails.version >= '5.1.0' - config.eager_load_paths.reject! { |p| p =~ %r{/app/(\w+)$} && !%w(controllers helpers views).push(CI_ORM).include?(Regexp.last_match[1]) } - config.autoload_paths += %W(#{config.root}/app/#{CI_ORM} #{config.root}/app/#{CI_ORM}/concerns #{config.root}/lib) + config.load_defaults Rails.version[0, 3] + config.eager_load_paths = (config.try(:all_eager_load_paths) || config.eager_load_paths).reject { |p| p =~ %r{/app/([^/]+)} && !%W[controllers jobs locales mailers #{CI_ORM}].include?(Regexp.last_match[1]) } + config.eager_load_paths += %W[#{config.root}/app/eager_loaded] + config.autoload_paths += %W[#{config.root}/lib] config.i18n.load_path += Dir[Rails.root.join('app', 'locales', '*.{rb,yml}').to_s] - config.active_record.time_zone_aware_types = [:datetime, :time] if CI_ORM == :active_record - config.active_record.sqlite3.represent_boolean_as_integer = true if CI_ORM == :active_record && Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR == 2 + if CI_ORM == :active_record + config.active_record.time_zone_aware_types = %i[datetime time] + config.active_record.yaml_column_permitted_classes = [Symbol] if [ActiveRecord::Base, ActiveRecord].any? { |klass| klass.respond_to?(:yaml_column_permitted_classes=) } + end config.active_storage.service = :local if defined?(ActiveStorage) + config.active_storage.replace_on_assign_to_many = false if defined?(ActiveStorage) && ActiveStorage.version < Gem::Version.create('6.1') + + case CI_ASSET + when :webpack + config.assets.precompile += %w[rails_admin.js rails_admin.css] + when :importmap + config.assets.paths << RailsAdmin::Engine.root.join('src') + config.assets.precompile += %w[rails_admin.js rails_admin.css] + config.importmap.cache_sweepers << RailsAdmin::Engine.root.join('src') + end + + initializer :ignore_unused_assets_path, after: :append_assets_path, group: :all do |app| + case CI_ASSET + when :webpack, :importmap + app.config.assets.paths.delete(Rails.root.join('app', 'assets', 'javascripts').to_s) + when :sprockets + app.config.assets.paths.delete(Rails.root.join('app', 'assets', 'builds').to_s) + end + end end end diff --git a/spec/dummy_app/config/boot.rb b/spec/dummy_app/config/boot.rb index 8e888c88d4..f948530371 100644 --- a/spec/dummy_app/config/boot.rb +++ b/spec/dummy_app/config/boot.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + CI_ORM = (ENV['CI_ORM'] || :active_record).to_sym unless defined?(CI_ORM) -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +CI_ASSET = (ENV['CI_ASSET'] || :sprockets).to_sym unless defined?(CI_ASSET) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. diff --git a/spec/dummy_app/config/database.yml b/spec/dummy_app/config/database.yml index 23110f4017..d75ec1f7e2 100644 --- a/spec/dummy_app/config/database.yml +++ b/spec/dummy_app/config/database.yml @@ -30,3 +30,7 @@ development: test: <<: *sqlite database: db/test.sqlite3 + +production: + <<: *sqlite + database: db/production.sqlite3 diff --git a/spec/dummy_app/config/dockerfile.yml b/spec/dummy_app/config/dockerfile.yml new file mode 100644 index 0000000000..04f4fe9f77 --- /dev/null +++ b/spec/dummy_app/config/dockerfile.yml @@ -0,0 +1,6 @@ +# generated by dockerfile-rails +--- +options: + label: + fly_launch_runtime: rails + sentry: false diff --git a/spec/dummy_app/config/environment.rb b/spec/dummy_app/config/environment.rb index e82229a481..3cd6c0d79a 100644 --- a/spec/dummy_app/config/environment.rb +++ b/spec/dummy_app/config/environment.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + # Load the Rails application. -require File.expand_path('../application', __FILE__) +require File.expand_path('application', __dir__) # Initialize the Rails application. DummyApp::Application.initialize! diff --git a/spec/dummy_app/config/environments/development.rb b/spec/dummy_app/config/environments/development.rb index bd8043d9af..8316c2cf15 100644 --- a/spec/dummy_app/config/environments/development.rb +++ b/spec/dummy_app/config/environments/development.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + DummyApp::Application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -31,19 +33,21 @@ # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load if CI_ORM == :active_record - # Debug mode disables concatenation and preprocessing of assets. - # This option may cause significant delays in view rendering with a large - # number of complex assets. - config.assets.debug = true + if config.respond_to?(:assets) + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true - # Asset digests allow you to set far-future HTTP expiration dates on all assets, - # yet still be able to expire them through the digest params. - config.assets.digest = true + # Asset digests allow you to set far-future HTTP expiration dates on all assets, + # yet still be able to expire them through the digest params. + config.assets.digest = true - # Adds additional error checking when serving assets at runtime. - # Checks for improperly declared sprockets dependencies. - # Raises helpful error messages. - config.assets.raise_runtime_errors = true + # Adds additional error checking when serving assets at runtime. + # Checks for improperly declared sprockets dependencies. + # Raises helpful error messages. + config.assets.raise_runtime_errors = true + end # Raises error for missing translations # config.action_view.raise_on_missing_translations = true diff --git a/spec/dummy_app/config/environments/production.rb b/spec/dummy_app/config/environments/production.rb index d2a86e9fd1..e24a00b7e5 100644 --- a/spec/dummy_app/config/environments/production.rb +++ b/spec/dummy_app/config/environments/production.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + DummyApp::Application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -20,7 +22,7 @@ config.static_cache_control = 'public, max-age=31536000' # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + # config.assets.js_compressor = :uglifier # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. diff --git a/spec/dummy_app/config/environments/test.rb b/spec/dummy_app/config/environments/test.rb index 17a80c10ce..03736ffa98 100644 --- a/spec/dummy_app/config/environments/test.rb +++ b/spec/dummy_app/config/environments/test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + DummyApp::Application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -26,7 +28,7 @@ config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates. - config.action_dispatch.show_exceptions = false + config.action_dispatch.show_exceptions = Rails.gem_version >= Gem::Version.new('7.1') ? :none : false # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false diff --git a/spec/dummy_app/config/importmap.rails_admin.rb b/spec/dummy_app/config/importmap.rails_admin.rb new file mode 100644 index 0000000000..7b1a8a1cf1 --- /dev/null +++ b/spec/dummy_app/config/importmap.rails_admin.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# Pin npm packages by running ./bin/importmap + +pin 'rails_admin', preload: true +pin 'rails_admin/src/rails_admin/base', to: 'rails_admin/base.js' +pin '@hotwired/turbo', to: 'https://ga.jspm.io/npm:@hotwired/turbo@7.1.0/dist/turbo.es2017-esm.js' +pin '@hotwired/turbo-rails', to: 'https://ga.jspm.io/npm:@hotwired/turbo-rails@7.1.3/app/javascript/turbo/index.js' +pin '@popperjs/core', to: 'https://ga.jspm.io/npm:@popperjs/core@2.11.5/dist/esm/popper.js' +pin '@rails/actioncable/src', to: 'https://ga.jspm.io/npm:@rails/actioncable@7.0.3-1/src/index.js' +pin '@rails/actiontext', to: 'https://ga.jspm.io/npm:@rails/actiontext@7.0.3-1/app/javascript/actiontext/index.js' +pin '@rails/activestorage', to: 'https://ga.jspm.io/npm:@rails/activestorage@7.0.3-1/app/assets/javascripts/activestorage.esm.js' +pin '@rails/ujs', to: 'https://ga.jspm.io/npm:@rails/ujs@7.0.3-1/lib/assets/compiled/rails-ujs.js' +pin 'bootstrap', to: 'https://ga.jspm.io/npm:bootstrap@5.1.3/dist/js/bootstrap.esm.js' +pin 'flatpickr', to: 'https://ga.jspm.io/npm:flatpickr@4.6.13/dist/flatpickr.js' +pin 'flatpickr/', to: 'https://ga.jspm.io/npm:flatpickr@4.6.13/' +pin 'jquery', to: 'https://ga.jspm.io/npm:jquery@3.6.0/dist/jquery.js' +pin 'jquery-ui/', to: 'https://ga.jspm.io/npm:jquery-ui@1.13.1/' +pin 'trix', to: 'https://ga.jspm.io/npm:trix@2.0.0-beta.0/dist/trix.js' diff --git a/spec/dummy_app/config/importmap.rb b/spec/dummy_app/config/importmap.rb new file mode 100644 index 0000000000..a7309e8e9e --- /dev/null +++ b/spec/dummy_app/config/importmap.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# Pin npm packages by running ./bin/importmap + +pin 'application', preload: true +pin '@hotwired/turbo-rails', to: 'https://ga.jspm.io/npm:@hotwired/turbo-rails@7.1.3/app/javascript/turbo/index.js' +pin '@rails/ujs', to: 'https://ga.jspm.io/npm:@rails/ujs@6.0.5/lib/assets/compiled/rails-ujs.js' +pin '@hotwired/turbo', to: 'https://ga.jspm.io/npm:@hotwired/turbo@7.1.0/dist/turbo.es2017-esm.js' +pin '@rails/actioncable/src', to: 'https://ga.jspm.io/npm:@rails/actioncable@7.0.3/src/index.js' diff --git a/spec/dummy_app/config/initializers/application_controller_renderer.rb b/spec/dummy_app/config/initializers/application_controller_renderer.rb index 51639b67a0..6e2d5d2b87 100644 --- a/spec/dummy_app/config/initializers/application_controller_renderer.rb +++ b/spec/dummy_app/config/initializers/application_controller_renderer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # ApplicationController.renderer.defaults.merge!( diff --git a/spec/dummy_app/config/initializers/assets.rb b/spec/dummy_app/config/initializers/assets.rb index 01ef3e6630..3dd6c18505 100644 --- a/spec/dummy_app/config/initializers/assets.rb +++ b/spec/dummy_app/config/initializers/assets.rb @@ -1,10 +1,13 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = '1.0' +Rails.application.config.assets.version = '1.0' if Rails.application.config.respond_to?(:assets) # Add additional assets to the asset load path # Rails.application.config.assets.paths << Emoji.images_path +Rails.application.config.assets.paths << Rails.root.join('node_modules/@fortawesome/fontawesome-free/webfonts') if Rails.application.config.respond_to?(:assets) # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. diff --git a/spec/dummy_app/config/initializers/backtrace_silencers.rb b/spec/dummy_app/config/initializers/backtrace_silencers.rb index 59385cdf37..4b63f2893d 100644 --- a/spec/dummy_app/config/initializers/backtrace_silencers.rb +++ b/spec/dummy_app/config/initializers/backtrace_silencers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. diff --git a/spec/dummy_app/config/initializers/cookies_serializer.rb b/spec/dummy_app/config/initializers/cookies_serializer.rb index 93329258f7..fd2799e50e 100644 --- a/spec/dummy_app/config/initializers/cookies_serializer.rb +++ b/spec/dummy_app/config/initializers/cookies_serializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # This is a new Rails 5.0 default, so introduced as a config to ensure apps made with earlier versions of Rails aren't affected when upgrading. diff --git a/spec/dummy_app/config/initializers/cors.rb b/spec/dummy_app/config/initializers/cors.rb index 3b1c1b5ed1..82eafe5ca4 100644 --- a/spec/dummy_app/config/initializers/cors.rb +++ b/spec/dummy_app/config/initializers/cors.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Avoid CORS issues when API is called from the frontend app. diff --git a/spec/dummy_app/config/initializers/devise.rb b/spec/dummy_app/config/initializers/devise.rb index ed78238de4..ff297132e7 100644 --- a/spec/dummy_app/config/initializers/devise.rb +++ b/spec/dummy_app/config/initializers/devise.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Use this hook to configure devise mailer, warden hooks and so forth. # Many of these configuration options can be set straight in your model. Devise.setup do |config| diff --git a/spec/dummy_app/config/initializers/dragonfly.rb b/spec/dummy_app/config/initializers/dragonfly.rb index ae571e1627..16209b474a 100644 --- a/spec/dummy_app/config/initializers/dragonfly.rb +++ b/spec/dummy_app/config/initializers/dragonfly.rb @@ -1,5 +1,10 @@ +# frozen_string_literal: true + require 'dragonfly' +# Logger +Dragonfly.logger = Rails.logger + # Configure Dragonfly.app.configure do plugin :imagemagick @@ -14,8 +19,5 @@ server_root: Rails.root.join('public')) end -# Logger -Dragonfly.logger = Rails.logger - # Mount as middleware Rails.application.middleware.use Dragonfly::Middleware diff --git a/spec/dummy_app/config/initializers/filter_parameter_logging.rb b/spec/dummy_app/config/initializers/filter_parameter_logging.rb index 4a994e1e7b..7a4f47b4c2 100644 --- a/spec/dummy_app/config/initializers/filter_parameter_logging.rb +++ b/spec/dummy_app/config/initializers/filter_parameter_logging.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. diff --git a/spec/dummy_app/config/initializers/inflections.rb b/spec/dummy_app/config/initializers/inflections.rb index ac033bf9dc..dc84742212 100644 --- a/spec/dummy_app/config/initializers/inflections.rb +++ b/spec/dummy_app/config/initializers/inflections.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections diff --git a/spec/dummy_app/config/initializers/mime_types.rb b/spec/dummy_app/config/initializers/mime_types.rb index dc1899682b..be6fedc535 100644 --- a/spec/dummy_app/config/initializers/mime_types.rb +++ b/spec/dummy_app/config/initializers/mime_types.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: diff --git a/spec/dummy_app/config/initializers/mongoid.rb b/spec/dummy_app/config/initializers/mongoid.rb index 07246eec2e..6d0f86ec6a 100644 --- a/spec/dummy_app/config/initializers/mongoid.rb +++ b/spec/dummy_app/config/initializers/mongoid.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + if CI_ORM == :mongoid filename = if Mongoid.respond_to?(:belongs_to_required_by_default=) - "mongoid6.yml" + 'mongoid6.yml' else - "mongoid5.yml" + 'mongoid5.yml' end - ::Mongoid.load!(Rails.root.join("config", filename)) + ::Mongoid.load!(Rails.root.join('config', filename)) end diff --git a/spec/dummy_app/config/initializers/paper_trail.rb b/spec/dummy_app/config/initializers/paper_trail.rb deleted file mode 100644 index 69366b352e..0000000000 --- a/spec/dummy_app/config/initializers/paper_trail.rb +++ /dev/null @@ -1 +0,0 @@ -PaperTrail.config.track_associations = false if defined?(PaperTrail) && PaperTrail::VERSION::MAJOR < 10 diff --git a/spec/dummy_app/config/initializers/rails_admin.rb b/spec/dummy_app/config/initializers/rails_admin.rb index 95f6c1dbdc..9e8798c13b 100644 --- a/spec/dummy_app/config/initializers/rails_admin.rb +++ b/spec/dummy_app/config/initializers/rails_admin.rb @@ -1,6 +1,20 @@ +# frozen_string_literal: true + RailsAdmin.config do |c| + c.asset_source = CI_ASSET c.model Team do include_all_fields - field :color, :color + field :color, :hidden + end + + if Rails.env.production? + # Live demo configuration + c.main_app_name = ['RailsAdmin', 'Live Demo'] + c.included_models = %w[Comment Division Draft Fan FieldTest League NestedFieldTest Player Team User] + c.model 'FieldTest' do + configure :paperclip_asset do + visible false + end + end end end diff --git a/spec/dummy_app/config/initializers/secret_token.rb b/spec/dummy_app/config/initializers/secret_token.rb index 36ffbe19b2..8c8d601d88 100644 --- a/spec/dummy_app/config/initializers/secret_token.rb +++ b/spec/dummy_app/config/initializers/secret_token.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Your secret key is used for verifying the integrity of signed cookies. diff --git a/spec/dummy_app/config/initializers/session_patch.rb b/spec/dummy_app/config/initializers/session_patch.rb index 26483b65c4..26a4cb2b63 100644 --- a/spec/dummy_app/config/initializers/session_patch.rb +++ b/spec/dummy_app/config/initializers/session_patch.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'action_dispatch/middleware/session/abstract_store' # When ORM was switched, but another ORM's model class still exists in session diff --git a/spec/dummy_app/config/initializers/session_store.rb b/spec/dummy_app/config/initializers/session_store.rb index 13485264fa..2944570d20 100644 --- a/spec/dummy_app/config/initializers/session_store.rb +++ b/spec/dummy_app/config/initializers/session_store.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. Rails.application.config.session_store :cookie_store, key: '_dummy_app_session' diff --git a/spec/dummy_app/config/initializers/shrine.rb b/spec/dummy_app/config/initializers/shrine.rb index 901b6738e4..7a904af958 100644 --- a/spec/dummy_app/config/initializers/shrine.rb +++ b/spec/dummy_app/config/initializers/shrine.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'shrine' require 'shrine/storage/memory' diff --git a/spec/dummy_app/config/initializers/wrap_parameters.rb b/spec/dummy_app/config/initializers/wrap_parameters.rb index 6b3f5353d0..873da24045 100644 --- a/spec/dummy_app/config/initializers/wrap_parameters.rb +++ b/spec/dummy_app/config/initializers/wrap_parameters.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which diff --git a/spec/dummy_app/config/locales/devise.en.yml b/spec/dummy_app/config/locales/devise.en.yml index d5b4520cb3..4147fff8cc 100644 --- a/spec/dummy_app/config/locales/devise.en.yml +++ b/spec/dummy_app/config/locales/devise.en.yml @@ -25,7 +25,7 @@ en: unlock_instructions: subject: "Unlock Instructions" omniauth_callbacks: - failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + failure: 'Could not authenticate you from %{kind} because "%{reason}".' success: "Successfully authenticated from %{kind} account." passwords: no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." diff --git a/spec/dummy_app/config/locales/en.yml b/spec/dummy_app/config/locales/en.yml index 6cbff6557a..3542c7a020 100644 --- a/spec/dummy_app/config/locales/en.yml +++ b/spec/dummy_app/config/locales/en.yml @@ -2,4 +2,4 @@ en: admin: help: team: - name: "Team Name Help Text." \ No newline at end of file + name: "Team Name Help Text." diff --git a/spec/dummy_app/config/locales/fr.yml b/spec/dummy_app/config/locales/fr.yml index a037fed03e..6da1c0ca80 100644 --- a/spec/dummy_app/config/locales/fr.yml +++ b/spec/dummy_app/config/locales/fr.yml @@ -7,8 +7,38 @@ fr: long: "%e %B %Y" day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi] abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam] - month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre] - abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.] + month_names: + [ + ~, + janvier, + février, + mars, + avril, + mai, + juin, + juillet, + août, + septembre, + octobre, + novembre, + décembre, + ] + abbr_month_names: + [ + ~, + jan., + fév., + mar., + avr., + mai, + juin, + juil., + août, + sept., + oct., + nov., + déc., + ] order: - :day - :month @@ -20,5 +50,5 @@ fr: compact: "%d/%m/%y %H:%M" long: "%A %d %B %Y %H:%M" verb: "%d %B %Y" - am: 'am' - pm: 'pm' + am: "am" + pm: "pm" diff --git a/spec/dummy_app/config/routes.rb b/spec/dummy_app/config/routes.rb index 3f78eae46b..30a51f9d56 100644 --- a/spec/dummy_app/config/routes.rb +++ b/spec/dummy_app/config/routes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + DummyApp::Application.routes.draw do # Needed for :show_in_app tests resources :players, only: [:show] diff --git a/spec/dummy_app/config/vite.json b/spec/dummy_app/config/vite.json new file mode 100644 index 0000000000..227b1370f6 --- /dev/null +++ b/spec/dummy_app/config/vite.json @@ -0,0 +1,15 @@ +{ + "all": { + "sourceCodeDir": "app/frontend", + "watchAdditionalPaths": ["../../src"] + }, + "development": { + "autoBuild": true, + "publicOutputDir": "vite", + "port": 3036 + }, + "test": { + "autoBuild": false, + "publicOutputDir": "vite" + } +} diff --git a/spec/dummy_app/config/webpack/development.js b/spec/dummy_app/config/webpack/development.js new file mode 100644 index 0000000000..7f3342f96f --- /dev/null +++ b/spec/dummy_app/config/webpack/development.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || "development"; + +const environment = require("./environment"); + +module.exports = environment.toWebpackConfig(); diff --git a/spec/dummy_app/config/webpack/environment.js b/spec/dummy_app/config/webpack/environment.js new file mode 100644 index 0000000000..4c26baad24 --- /dev/null +++ b/spec/dummy_app/config/webpack/environment.js @@ -0,0 +1,3 @@ +const { environment } = require("@rails/webpacker"); + +module.exports = environment; diff --git a/spec/dummy_app/config/webpack/production.js b/spec/dummy_app/config/webpack/production.js new file mode 100644 index 0000000000..73d924d834 --- /dev/null +++ b/spec/dummy_app/config/webpack/production.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || "production"; + +const environment = require("./environment"); + +module.exports = environment.toWebpackConfig(); diff --git a/spec/dummy_app/config/webpack/test.js b/spec/dummy_app/config/webpack/test.js new file mode 100644 index 0000000000..7f3342f96f --- /dev/null +++ b/spec/dummy_app/config/webpack/test.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || "development"; + +const environment = require("./environment"); + +module.exports = environment.toWebpackConfig(); diff --git a/spec/dummy_app/config/webpacker.yml b/spec/dummy_app/config/webpacker.yml new file mode 100644 index 0000000000..1d6e322ed4 --- /dev/null +++ b/spec/dummy_app/config/webpacker.yml @@ -0,0 +1,92 @@ +# Note: You must restart bin/webpack-dev-server for changes to take effect + +default: &default + source_path: app/javascript + source_entry_path: . + public_root_path: public + public_output_path: packs + cache_path: tmp/cache/webpacker + webpack_compile_output: true + + # Additional paths webpack should lookup modules + # ['app/assets', 'engine/foo/app/assets'] + additional_paths: ["node_modules/rails_admin/src"] + + # Reload manifest.json on all requests so we reload latest compiled packs + cache_manifest: false + + # Extract and emit a css file + extract_css: true + + static_assets_extensions: + - .jpg + - .jpeg + - .png + - .gif + - .tiff + - .ico + - .svg + - .eot + - .otf + - .ttf + - .woff + - .woff2 + + extensions: + - .mjs + - .js + - .sass + - .scss + - .css + - .module.sass + - .module.scss + - .module.css + - .png + - .svg + - .gif + - .jpeg + - .jpg + +development: + <<: *default + compile: true + + # Reference: https://webpack.js.org/configuration/dev-server/ + dev_server: + https: false + host: localhost + port: 3035 + public: localhost:3035 + hmr: false + # Inline should be set to true if using HMR + inline: true + overlay: true + compress: true + disable_host_check: true + use_local_ip: false + quiet: false + pretty: false + headers: + "Access-Control-Allow-Origin": "*" + watch_options: + ignored: "**/node_modules/**" + +test: + <<: *default + compile: true + additional_paths: ["src"] # relative from the project root + + # Compile test packs to a separate directory + public_output_path: packs-test + +production: + <<: *default + + # Production depends on precompilation of packs prior to booting for performance. + compile: false + + # Extract and emit a css file + extract_css: true + + # Cache manifest.json for performance + cache_manifest: true diff --git a/spec/dummy_app/db/migrate/00000000000001_create_divisions_migration.rb b/spec/dummy_app/db/migrate/00000000000001_create_divisions_migration.rb index 30a9cd4369..f654f54b9b 100644 --- a/spec/dummy_app/db/migrate/00000000000001_create_divisions_migration.rb +++ b/spec/dummy_app/db/migrate/00000000000001_create_divisions_migration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateDivisionsMigration < ActiveRecord::Migration[5.0] def self.up create_table :divisions do |t| diff --git a/spec/dummy_app/db/migrate/00000000000002_create_drafts_migration.rb b/spec/dummy_app/db/migrate/00000000000002_create_drafts_migration.rb index fdc4b568cb..9e9dc150e0 100644 --- a/spec/dummy_app/db/migrate/00000000000002_create_drafts_migration.rb +++ b/spec/dummy_app/db/migrate/00000000000002_create_drafts_migration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateDraftsMigration < ActiveRecord::Migration[5.0] def self.up create_table :drafts do |t| diff --git a/spec/dummy_app/db/migrate/00000000000003_create_leagues_migration.rb b/spec/dummy_app/db/migrate/00000000000003_create_leagues_migration.rb index 6dda40b27f..87eefe1176 100644 --- a/spec/dummy_app/db/migrate/00000000000003_create_leagues_migration.rb +++ b/spec/dummy_app/db/migrate/00000000000003_create_leagues_migration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateLeaguesMigration < ActiveRecord::Migration[5.0] def self.up create_table :leagues do |t| diff --git a/spec/dummy_app/db/migrate/00000000000004_create_players_migration.rb b/spec/dummy_app/db/migrate/00000000000004_create_players_migration.rb index f72e6a3580..47fe8ca33f 100644 --- a/spec/dummy_app/db/migrate/00000000000004_create_players_migration.rb +++ b/spec/dummy_app/db/migrate/00000000000004_create_players_migration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreatePlayersMigration < ActiveRecord::Migration[5.0] def self.up create_table :players do |t| diff --git a/spec/dummy_app/db/migrate/00000000000005_create_teams_migration.rb b/spec/dummy_app/db/migrate/00000000000005_create_teams_migration.rb index e988ac6bc7..c2ff3039c1 100644 --- a/spec/dummy_app/db/migrate/00000000000005_create_teams_migration.rb +++ b/spec/dummy_app/db/migrate/00000000000005_create_teams_migration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateTeamsMigration < ActiveRecord::Migration[5.0] def self.up create_table :teams do |t| diff --git a/spec/dummy_app/db/migrate/00000000000006_devise_create_users.rb b/spec/dummy_app/db/migrate/00000000000006_devise_create_users.rb index 5cc9422390..22efc21756 100644 --- a/spec/dummy_app/db/migrate/00000000000006_devise_create_users.rb +++ b/spec/dummy_app/db/migrate/00000000000006_devise_create_users.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DeviseCreateUsers < ActiveRecord::Migration[5.0] def self.up create_table :users do |t| diff --git a/spec/dummy_app/db/migrate/00000000000007_create_histories_table.rb b/spec/dummy_app/db/migrate/00000000000007_create_histories_table.rb index 92fe8679b5..b19701c394 100644 --- a/spec/dummy_app/db/migrate/00000000000007_create_histories_table.rb +++ b/spec/dummy_app/db/migrate/00000000000007_create_histories_table.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateHistoriesTable < ActiveRecord::Migration[5.0] def self.up create_table :histories do |t| @@ -7,7 +9,7 @@ def self.up t.string :table t.timestamps null: false end - add_index(:histories, [:item, :table]) + add_index(:histories, %i[item table]) end def self.down diff --git a/spec/dummy_app/db/migrate/00000000000008_create_fans_migration.rb b/spec/dummy_app/db/migrate/00000000000008_create_fans_migration.rb index f49dd7d519..a838b09f19 100644 --- a/spec/dummy_app/db/migrate/00000000000008_create_fans_migration.rb +++ b/spec/dummy_app/db/migrate/00000000000008_create_fans_migration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateFansMigration < ActiveRecord::Migration[5.0] def self.up create_table :fans do |t| diff --git a/spec/dummy_app/db/migrate/00000000000009_create_fans_teams_migration.rb b/spec/dummy_app/db/migrate/00000000000009_create_fans_teams_migration.rb index 258fec6f57..de26b42fe0 100644 --- a/spec/dummy_app/db/migrate/00000000000009_create_fans_teams_migration.rb +++ b/spec/dummy_app/db/migrate/00000000000009_create_fans_teams_migration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateFansTeamsMigration < ActiveRecord::Migration[5.0] def self.up create_table :fans_teams, id: false do |t| diff --git a/spec/dummy_app/db/migrate/00000000000010_add_revenue_to_team_migration.rb b/spec/dummy_app/db/migrate/00000000000010_add_revenue_to_team_migration.rb index c76cd671d3..2f96e52d34 100644 --- a/spec/dummy_app/db/migrate/00000000000010_add_revenue_to_team_migration.rb +++ b/spec/dummy_app/db/migrate/00000000000010_add_revenue_to_team_migration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddRevenueToTeamMigration < ActiveRecord::Migration[5.0] def self.up add_column :teams, :revenue, :decimal, precision: 18, scale: 2 diff --git a/spec/dummy_app/db/migrate/00000000000011_add_suspended_to_player_migration.rb b/spec/dummy_app/db/migrate/00000000000011_add_suspended_to_player_migration.rb index 12676d4641..e8f8ad1fb4 100644 --- a/spec/dummy_app/db/migrate/00000000000011_add_suspended_to_player_migration.rb +++ b/spec/dummy_app/db/migrate/00000000000011_add_suspended_to_player_migration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddSuspendedToPlayerMigration < ActiveRecord::Migration[5.0] def self.up add_column :players, :suspended, :boolean, default: false diff --git a/spec/dummy_app/db/migrate/00000000000012_add_avatar_columns_to_user.rb b/spec/dummy_app/db/migrate/00000000000012_add_avatar_columns_to_user.rb index a85353539d..401e1eaae7 100644 --- a/spec/dummy_app/db/migrate/00000000000012_add_avatar_columns_to_user.rb +++ b/spec/dummy_app/db/migrate/00000000000012_add_avatar_columns_to_user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddAvatarColumnsToUser < ActiveRecord::Migration[5.0] def self.up add_column :users, :avatar_file_name, :string diff --git a/spec/dummy_app/db/migrate/00000000000013_add_roles_to_user.rb b/spec/dummy_app/db/migrate/00000000000013_add_roles_to_user.rb index fce0819e1d..64f3f3afce 100644 --- a/spec/dummy_app/db/migrate/00000000000013_add_roles_to_user.rb +++ b/spec/dummy_app/db/migrate/00000000000013_add_roles_to_user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddRolesToUser < ActiveRecord::Migration[5.0] def self.up add_column :users, :roles, :string diff --git a/spec/dummy_app/db/migrate/00000000000014_add_color_to_team_migration.rb b/spec/dummy_app/db/migrate/00000000000014_add_color_to_team_migration.rb index 43b3e3f6d6..c8bdfa05ca 100644 --- a/spec/dummy_app/db/migrate/00000000000014_add_color_to_team_migration.rb +++ b/spec/dummy_app/db/migrate/00000000000014_add_color_to_team_migration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddColorToTeamMigration < ActiveRecord::Migration[5.0] def self.up add_column :teams, :color, :string diff --git a/spec/dummy_app/db/migrate/20101223222233_create_rel_tests.rb b/spec/dummy_app/db/migrate/20101223222233_create_rel_tests.rb index 4cf152bba9..8b9dbf242b 100644 --- a/spec/dummy_app/db/migrate/20101223222233_create_rel_tests.rb +++ b/spec/dummy_app/db/migrate/20101223222233_create_rel_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateRelTests < ActiveRecord::Migration[5.0] def self.up create_table :rel_tests do |t| diff --git a/spec/dummy_app/db/migrate/20110103205808_create_comments.rb b/spec/dummy_app/db/migrate/20110103205808_create_comments.rb index 48d3b4a58b..39936fbe82 100644 --- a/spec/dummy_app/db/migrate/20110103205808_create_comments.rb +++ b/spec/dummy_app/db/migrate/20110103205808_create_comments.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateComments < ActiveRecord::Migration[5.0] def self.up create_table :comments do |t| diff --git a/spec/dummy_app/db/migrate/20110123042530_rename_histories_to_rails_admin_histories.rb b/spec/dummy_app/db/migrate/20110123042530_rename_histories_to_rails_admin_histories.rb index 1b04b2349a..d27a550345 100644 --- a/spec/dummy_app/db/migrate/20110123042530_rename_histories_to_rails_admin_histories.rb +++ b/spec/dummy_app/db/migrate/20110123042530_rename_histories_to_rails_admin_histories.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RenameHistoriesToRailsAdminHistories < ActiveRecord::Migration[5.0] def self.up rename_table :histories, :rails_admin_histories diff --git a/spec/dummy_app/db/migrate/20110224184303_create_field_tests.rb b/spec/dummy_app/db/migrate/20110224184303_create_field_tests.rb index 04b6fa5f1e..e4213c03f3 100644 --- a/spec/dummy_app/db/migrate/20110224184303_create_field_tests.rb +++ b/spec/dummy_app/db/migrate/20110224184303_create_field_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateFieldTests < ActiveRecord::Migration[5.0] def self.up create_table :field_tests do |t| diff --git a/spec/dummy_app/db/migrate/20110328193014_create_cms_basic_pages.rb b/spec/dummy_app/db/migrate/20110328193014_create_cms_basic_pages.rb index a71089883f..85656966bc 100644 --- a/spec/dummy_app/db/migrate/20110328193014_create_cms_basic_pages.rb +++ b/spec/dummy_app/db/migrate/20110328193014_create_cms_basic_pages.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateCmsBasicPages < ActiveRecord::Migration[5.0] def self.up create_table :cms_basic_pages do |t| diff --git a/spec/dummy_app/db/migrate/20110329183136_remove_league_id_from_teams.rb b/spec/dummy_app/db/migrate/20110329183136_remove_league_id_from_teams.rb index 9b5ef55d22..15d3a931ad 100644 --- a/spec/dummy_app/db/migrate/20110329183136_remove_league_id_from_teams.rb +++ b/spec/dummy_app/db/migrate/20110329183136_remove_league_id_from_teams.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RemoveLeagueIdFromTeams < ActiveRecord::Migration[5.0] def self.up remove_column :teams, :league_id diff --git a/spec/dummy_app/db/migrate/20110607152842_add_format_to_field_test.rb b/spec/dummy_app/db/migrate/20110607152842_add_format_to_field_test.rb index be5c9c1206..f0762e0f83 100644 --- a/spec/dummy_app/db/migrate/20110607152842_add_format_to_field_test.rb +++ b/spec/dummy_app/db/migrate/20110607152842_add_format_to_field_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddFormatToFieldTest < ActiveRecord::Migration[5.0] def self.up add_column :field_tests, :format, :string diff --git a/spec/dummy_app/db/migrate/20110714095433_create_balls.rb b/spec/dummy_app/db/migrate/20110714095433_create_balls.rb index 206cb84b1d..7b2ba38ebd 100644 --- a/spec/dummy_app/db/migrate/20110714095433_create_balls.rb +++ b/spec/dummy_app/db/migrate/20110714095433_create_balls.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateBalls < ActiveRecord::Migration[5.0] def self.up create_table :balls, force: true do |t| diff --git a/spec/dummy_app/db/migrate/20110831090841_add_protected_field_and_restricted_field_to_field_tests.rb b/spec/dummy_app/db/migrate/20110831090841_add_protected_field_and_restricted_field_to_field_tests.rb index 9d5dd6988f..79321dde7d 100644 --- a/spec/dummy_app/db/migrate/20110831090841_add_protected_field_and_restricted_field_to_field_tests.rb +++ b/spec/dummy_app/db/migrate/20110831090841_add_protected_field_and_restricted_field_to_field_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddProtectedFieldAndRestrictedFieldToFieldTests < ActiveRecord::Migration[5.0] def change add_column :field_tests, :restricted_field, :string diff --git a/spec/dummy_app/db/migrate/20110901131551_change_division_primary_key.rb b/spec/dummy_app/db/migrate/20110901131551_change_division_primary_key.rb index d91f9191fd..cbc5d18e4a 100644 --- a/spec/dummy_app/db/migrate/20110901131551_change_division_primary_key.rb +++ b/spec/dummy_app/db/migrate/20110901131551_change_division_primary_key.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ChangeDivisionPrimaryKey < ActiveRecord::Migration[5.0] def up drop_table :divisions diff --git a/spec/dummy_app/db/migrate/20110901142530_rename_league_id_foreign_key_on_divisions.rb b/spec/dummy_app/db/migrate/20110901142530_rename_league_id_foreign_key_on_divisions.rb index 4892301522..7e965493c7 100644 --- a/spec/dummy_app/db/migrate/20110901142530_rename_league_id_foreign_key_on_divisions.rb +++ b/spec/dummy_app/db/migrate/20110901142530_rename_league_id_foreign_key_on_divisions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RenameLeagueIdForeignKeyOnDivisions < ActiveRecord::Migration[5.0] def change rename_column :divisions, :league_id, :custom_league_id diff --git a/spec/dummy_app/db/migrate/20110901150912_set_primary_key_not_null_for_divisions.rb b/spec/dummy_app/db/migrate/20110901150912_set_primary_key_not_null_for_divisions.rb index 1d5fb9cdaf..f6dc9a936d 100644 --- a/spec/dummy_app/db/migrate/20110901150912_set_primary_key_not_null_for_divisions.rb +++ b/spec/dummy_app/db/migrate/20110901150912_set_primary_key_not_null_for_divisions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SetPrimaryKeyNotNullForDivisions < ActiveRecord::Migration[5.0] def up drop_table :divisions diff --git a/spec/dummy_app/db/migrate/20110901154834_change_length_for_rails_admin_histories.rb b/spec/dummy_app/db/migrate/20110901154834_change_length_for_rails_admin_histories.rb index 4cf8d04c76..f6f21133cb 100644 --- a/spec/dummy_app/db/migrate/20110901154834_change_length_for_rails_admin_histories.rb +++ b/spec/dummy_app/db/migrate/20110901154834_change_length_for_rails_admin_histories.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ChangeLengthForRailsAdminHistories < ActiveRecord::Migration[5.0] def up change_column :rails_admin_histories, :message, :text diff --git a/spec/dummy_app/db/migrate/20111108143642_add_dragonfly_and_carrierwave_to_field_tests.rb b/spec/dummy_app/db/migrate/20111108143642_add_dragonfly_and_carrierwave_to_field_tests.rb index 43ff587718..d5e37bb9c5 100644 --- a/spec/dummy_app/db/migrate/20111108143642_add_dragonfly_and_carrierwave_to_field_tests.rb +++ b/spec/dummy_app/db/migrate/20111108143642_add_dragonfly_and_carrierwave_to_field_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddDragonflyAndCarrierwaveToFieldTests < ActiveRecord::Migration[5.0] def change add_column :field_tests, :paperclip_asset_file_name, :string diff --git a/spec/dummy_app/db/migrate/20111115041025_add_type_to_balls.rb b/spec/dummy_app/db/migrate/20111115041025_add_type_to_balls.rb index 5e87a13f51..4945e69bdd 100644 --- a/spec/dummy_app/db/migrate/20111115041025_add_type_to_balls.rb +++ b/spec/dummy_app/db/migrate/20111115041025_add_type_to_balls.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddTypeToBalls < ActiveRecord::Migration[5.0] def change add_column :balls, :type, :string diff --git a/spec/dummy_app/db/migrate/20111123092549_create_nested_field_tests.rb b/spec/dummy_app/db/migrate/20111123092549_create_nested_field_tests.rb index fa651387f2..b075eccf9c 100644 --- a/spec/dummy_app/db/migrate/20111123092549_create_nested_field_tests.rb +++ b/spec/dummy_app/db/migrate/20111123092549_create_nested_field_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateNestedFieldTests < ActiveRecord::Migration[5.0] def change create_table :nested_field_tests do |t| diff --git a/spec/dummy_app/db/migrate/20111130075338_add_dragonfly_asset_name_to_field_tests.rb b/spec/dummy_app/db/migrate/20111130075338_add_dragonfly_asset_name_to_field_tests.rb index 6dc7ffd276..f15fe8aa0b 100644 --- a/spec/dummy_app/db/migrate/20111130075338_add_dragonfly_asset_name_to_field_tests.rb +++ b/spec/dummy_app/db/migrate/20111130075338_add_dragonfly_asset_name_to_field_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddDragonflyAssetNameToFieldTests < ActiveRecord::Migration[5.0] def change add_column :field_tests, :dragonfly_asset_name, :string diff --git a/spec/dummy_app/db/migrate/20111215083258_create_foo_bars.rb b/spec/dummy_app/db/migrate/20111215083258_create_foo_bars.rb index 42822b3476..dc62a69012 100644 --- a/spec/dummy_app/db/migrate/20111215083258_create_foo_bars.rb +++ b/spec/dummy_app/db/migrate/20111215083258_create_foo_bars.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateFooBars < ActiveRecord::Migration[5.0] def change create_table :foo_bars do |t| diff --git a/spec/dummy_app/db/migrate/20120117151733_add_custom_field_to_teams.rb b/spec/dummy_app/db/migrate/20120117151733_add_custom_field_to_teams.rb index 4b5eb42b12..a5140974af 100644 --- a/spec/dummy_app/db/migrate/20120117151733_add_custom_field_to_teams.rb +++ b/spec/dummy_app/db/migrate/20120117151733_add_custom_field_to_teams.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddCustomFieldToTeams < ActiveRecord::Migration[5.0] def change add_column :teams, :custom_field, :string diff --git a/spec/dummy_app/db/migrate/20120118122004_add_categories.rb b/spec/dummy_app/db/migrate/20120118122004_add_categories.rb index c929adbd9b..e9e52bff70 100644 --- a/spec/dummy_app/db/migrate/20120118122004_add_categories.rb +++ b/spec/dummy_app/db/migrate/20120118122004_add_categories.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddCategories < ActiveRecord::Migration[5.0] def change create_table :categories do |t| diff --git a/spec/dummy_app/db/migrate/20120319041705_drop_rel_tests.rb b/spec/dummy_app/db/migrate/20120319041705_drop_rel_tests.rb index 13cb8c1fe7..0c68b8e91b 100644 --- a/spec/dummy_app/db/migrate/20120319041705_drop_rel_tests.rb +++ b/spec/dummy_app/db/migrate/20120319041705_drop_rel_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DropRelTests < ActiveRecord::Migration[5.0] def self.up drop_table :rel_tests diff --git a/spec/dummy_app/db/migrate/20120720075608_create_another_field_tests.rb b/spec/dummy_app/db/migrate/20120720075608_create_another_field_tests.rb index eb7e842588..6448df8a3f 100644 --- a/spec/dummy_app/db/migrate/20120720075608_create_another_field_tests.rb +++ b/spec/dummy_app/db/migrate/20120720075608_create_another_field_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateAnotherFieldTests < ActiveRecord::Migration[5.0] def change create_table :another_field_tests do |t| diff --git a/spec/dummy_app/db/migrate/20120928075608_create_images.rb b/spec/dummy_app/db/migrate/20120928075608_create_images.rb index 00b5cf1458..0673239a4a 100644 --- a/spec/dummy_app/db/migrate/20120928075608_create_images.rb +++ b/spec/dummy_app/db/migrate/20120928075608_create_images.rb @@ -1,7 +1,12 @@ +# frozen_string_literal: true + class CreateImages < ActiveRecord::Migration[5.0] def change create_table :images do |t| - t.attachment :file + t.string :file_file_name + t.string :file_content_type + t.bigint :file_file_size + t.datetime :file_updated_at t.timestamps null: false end end diff --git a/spec/dummy_app/db/migrate/20140412075608_create_deeply_nested_field_tests.rb b/spec/dummy_app/db/migrate/20140412075608_create_deeply_nested_field_tests.rb index bf8a483ae5..e6622203e2 100644 --- a/spec/dummy_app/db/migrate/20140412075608_create_deeply_nested_field_tests.rb +++ b/spec/dummy_app/db/migrate/20140412075608_create_deeply_nested_field_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateDeeplyNestedFieldTests < ActiveRecord::Migration[5.0] def change create_table :deeply_nested_field_tests do |t| diff --git a/spec/dummy_app/db/migrate/20140826093220_create_paper_trail_tests.rb b/spec/dummy_app/db/migrate/20140826093220_create_paper_trail_tests.rb index 8f7c4079cb..99dde5f42c 100644 --- a/spec/dummy_app/db/migrate/20140826093220_create_paper_trail_tests.rb +++ b/spec/dummy_app/db/migrate/20140826093220_create_paper_trail_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreatePaperTrailTests < ActiveRecord::Migration[5.0] def change create_table :paper_trail_tests do |t| diff --git a/spec/dummy_app/db/migrate/20140826093552_create_versions.rb b/spec/dummy_app/db/migrate/20140826093552_create_versions.rb index 69a49c85d7..9631ad9d8b 100644 --- a/spec/dummy_app/db/migrate/20140826093552_create_versions.rb +++ b/spec/dummy_app/db/migrate/20140826093552_create_versions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateVersions < ActiveRecord::Migration[5.0] def change create_table :versions do |t| @@ -8,6 +10,6 @@ def change t.text :object t.datetime :created_at end - add_index :versions, [:item_type, :item_id] + add_index :versions, %i[item_type item_id] end end diff --git a/spec/dummy_app/db/migrate/20150815102450_add_refile_to_field_tests.rb b/spec/dummy_app/db/migrate/20150815102450_add_refile_to_field_tests.rb index 90e74b9225..063bafb5e4 100644 --- a/spec/dummy_app/db/migrate/20150815102450_add_refile_to_field_tests.rb +++ b/spec/dummy_app/db/migrate/20150815102450_add_refile_to_field_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddRefileToFieldTests < ActiveRecord::Migration[5.0] def change add_column :field_tests, :refile_asset_id, :string diff --git a/spec/dummy_app/db/migrate/20151027181550_change_field_test_id_to_nested_field_tests.rb b/spec/dummy_app/db/migrate/20151027181550_change_field_test_id_to_nested_field_tests.rb index 1a9dba765c..b865623f4f 100644 --- a/spec/dummy_app/db/migrate/20151027181550_change_field_test_id_to_nested_field_tests.rb +++ b/spec/dummy_app/db/migrate/20151027181550_change_field_test_id_to_nested_field_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ChangeFieldTestIdToNestedFieldTests < ActiveRecord::Migration[5.0] def change change_column :nested_field_tests, :field_test_id, :integer, null: false diff --git a/spec/dummy_app/db/migrate/20160728152942_add_main_sponsor_to_teams.rb b/spec/dummy_app/db/migrate/20160728152942_add_main_sponsor_to_teams.rb index 920c667983..c8fd33799d 100644 --- a/spec/dummy_app/db/migrate/20160728152942_add_main_sponsor_to_teams.rb +++ b/spec/dummy_app/db/migrate/20160728152942_add_main_sponsor_to_teams.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddMainSponsorToTeams < ActiveRecord::Migration[5.0] def change add_column :teams, :main_sponsor, :integer, default: 0, null: false diff --git a/spec/dummy_app/db/migrate/20160728153058_add_formation_to_players.rb b/spec/dummy_app/db/migrate/20160728153058_add_formation_to_players.rb index cf894e306c..b024cab52c 100644 --- a/spec/dummy_app/db/migrate/20160728153058_add_formation_to_players.rb +++ b/spec/dummy_app/db/migrate/20160728153058_add_formation_to_players.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddFormationToPlayers < ActiveRecord::Migration[5.0] def change add_column :players, :formation, :string, default: 'substitute', null: false diff --git a/spec/dummy_app/db/migrate/20171229220713_add_enum_fields_to_field_tests.rb b/spec/dummy_app/db/migrate/20171229220713_add_enum_fields_to_field_tests.rb index 3bcb350334..5dcce3ab04 100644 --- a/spec/dummy_app/db/migrate/20171229220713_add_enum_fields_to_field_tests.rb +++ b/spec/dummy_app/db/migrate/20171229220713_add_enum_fields_to_field_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddEnumFieldsToFieldTests < ActiveRecord::Migration[5.0] def change add_column :field_tests, :string_enum_field, :string diff --git a/spec/dummy_app/db/migrate/20180701084251_create_active_storage_tables.active_storage.rb b/spec/dummy_app/db/migrate/20180701084251_create_active_storage_tables.active_storage.rb index 6419ee519b..adf13a3bdf 100644 --- a/spec/dummy_app/db/migrate/20180701084251_create_active_storage_tables.active_storage.rb +++ b/spec/dummy_app/db/migrate/20180701084251_create_active_storage_tables.active_storage.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This migration comes from active_storage (originally 20170806125915) class CreateActiveStorageTables < ActiveRecord::Migration[5.0] def change @@ -24,7 +26,7 @@ def change t.datetime :created_at, null: false - t.index [:record_type, :record_id, :name, :blob_id], name: "index_active_storage_attachments_uniqueness", unique: true + t.index %i[record_type record_id name blob_id], name: 'index_active_storage_attachments_uniqueness', unique: true end end end diff --git a/spec/dummy_app/db/migrate/20180707101855_add_carrierwave_assets_to_field_tests.rb b/spec/dummy_app/db/migrate/20180707101855_add_carrierwave_assets_to_field_tests.rb index 7f82555920..ed7f84d161 100644 --- a/spec/dummy_app/db/migrate/20180707101855_add_carrierwave_assets_to_field_tests.rb +++ b/spec/dummy_app/db/migrate/20180707101855_add_carrierwave_assets_to_field_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddCarrierwaveAssetsToFieldTests < ActiveRecord::Migration[5.0] def change add_column :field_tests, :carrierwave_assets, :string, after: :carrierwave_asset diff --git a/spec/dummy_app/db/migrate/20181029101829_add_shrine_data_to_field_tests.rb b/spec/dummy_app/db/migrate/20181029101829_add_shrine_data_to_field_tests.rb index f530a747db..30cd05a450 100644 --- a/spec/dummy_app/db/migrate/20181029101829_add_shrine_data_to_field_tests.rb +++ b/spec/dummy_app/db/migrate/20181029101829_add_shrine_data_to_field_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddShrineDataToFieldTests < ActiveRecord::Migration[5.0] def change add_column :field_tests, :shrine_asset_data, :text diff --git a/spec/dummy_app/db/migrate/20190531065324_create_action_text_tables.action_text.rb b/spec/dummy_app/db/migrate/20190531065324_create_action_text_tables.action_text.rb index b4cb74bbd4..897f35414b 100644 --- a/spec/dummy_app/db/migrate/20190531065324_create_action_text_tables.action_text.rb +++ b/spec/dummy_app/db/migrate/20190531065324_create_action_text_tables.action_text.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateActionTextTables < ActiveRecord::Migration[5.0] def change create_table :action_text_rich_texts do |t| @@ -7,7 +9,7 @@ def change t.timestamps - t.index [:record_type, :record_id, :name], name: "index_action_text_rich_texts_uniqueness", unique: true + t.index %i[record_type record_id name], name: 'index_action_text_rich_texts_uniqueness', unique: true end end end diff --git a/spec/dummy_app/db/migrate/20201127111952_update_active_storage_tables.rb b/spec/dummy_app/db/migrate/20201127111952_update_active_storage_tables.rb index 0e03cd9e9e..73262fe695 100644 --- a/spec/dummy_app/db/migrate/20201127111952_update_active_storage_tables.rb +++ b/spec/dummy_app/db/migrate/20201127111952_update_active_storage_tables.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class UpdateActiveStorageTables < ActiveRecord::Migration[5.0] def change add_column :active_storage_blobs, :service_name, :string, null: false, default: 'local' @@ -5,7 +7,7 @@ def change t.belongs_to :blob, null: false, index: false t.string :variation_digest, null: false - t.index %i[blob_id variation_digest], name: "index_active_storage_variant_records_uniqueness", unique: true + t.index %i[blob_id variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true end end end diff --git a/spec/dummy_app/db/migrate/20210811121027_create_two_level_namespaced_polymorphic_association_tests.rb b/spec/dummy_app/db/migrate/20210811121027_create_two_level_namespaced_polymorphic_association_tests.rb new file mode 100644 index 0000000000..1026c311a7 --- /dev/null +++ b/spec/dummy_app/db/migrate/20210811121027_create_two_level_namespaced_polymorphic_association_tests.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class CreateTwoLevelNamespacedPolymorphicAssociationTests < ActiveRecord::Migration[5.0] + def change + create_table :two_level_namespaced_polymorphic_association_tests do |t| + t.string :name + + t.timestamps + end + end +end diff --git a/spec/dummy_app/db/migrate/20210812115908_create_custom_versions.rb b/spec/dummy_app/db/migrate/20210812115908_create_custom_versions.rb new file mode 100644 index 0000000000..461cbafab7 --- /dev/null +++ b/spec/dummy_app/db/migrate/20210812115908_create_custom_versions.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class CreateCustomVersions < ActiveRecord::Migration[5.0] + def change + create_table :custom_versions do |t| + t.string :item_type, null: false + t.integer :item_id, null: false + t.string :event, null: false + t.string :whodunnit + t.text :object + t.datetime :created_at + end + add_index :custom_versions, %i[item_type item_id] + end +end diff --git a/spec/dummy_app/db/migrate/20211011235734_add_bool_field_open.rb b/spec/dummy_app/db/migrate/20211011235734_add_bool_field_open.rb new file mode 100644 index 0000000000..64a7e09ed1 --- /dev/null +++ b/spec/dummy_app/db/migrate/20211011235734_add_bool_field_open.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddBoolFieldOpen < ActiveRecord::Migration[6.0] + def change + add_column :field_tests, :open, :boolean + end +end diff --git a/spec/dummy_app/db/migrate/20220416102741_create_composite_key_tables.rb b/spec/dummy_app/db/migrate/20220416102741_create_composite_key_tables.rb new file mode 100644 index 0000000000..3586cf5d34 --- /dev/null +++ b/spec/dummy_app/db/migrate/20220416102741_create_composite_key_tables.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CreateCompositeKeyTables < ActiveRecord::Migration[6.0] + def change + add_column :fans_teams, :since, :date + + create_table :favorite_players, primary_key: %i[fan_id team_id player_id] do |t| + t.integer :fan_id, null: false + t.integer :team_id, null: false + t.integer :player_id, null: false + t.string :reason + + t.timestamps + end + end +end diff --git a/spec/dummy_app/db/migrate/20240921171953_add_non_nullable_boolean_field.rb b/spec/dummy_app/db/migrate/20240921171953_add_non_nullable_boolean_field.rb new file mode 100644 index 0000000000..c7f729d5a0 --- /dev/null +++ b/spec/dummy_app/db/migrate/20240921171953_add_non_nullable_boolean_field.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddNonNullableBooleanField < ActiveRecord::Migration[6.0] + def change + add_column :field_tests, :non_nullable_boolean_field, :boolean, null: false, default: false + end +end diff --git a/spec/dummy_app/db/seeds.rb b/spec/dummy_app/db/seeds.rb index 447fde0959..855a3f6d44 100644 --- a/spec/dummy_app/db/seeds.rb +++ b/spec/dummy_app/db/seeds.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'mlb' user_model = RailsAdmin::AbstractModel.new(User) @@ -16,15 +18,18 @@ results = connection.get('/sferik/mlb/e5b9384fc388f34ec5baca291343864135dcb0fe/cache/teams.json').body MLB::Team.send(:results_to_team, results).each do |mlb_team| - unless league = league_model.where(name: mlb_team.league).first + league = league_model.where(name: mlb_team.league).first + unless league league = league_model.model.new(name: mlb_team.league) league.save! end - unless division = division_model.where(name: mlb_team.division).first + division = division_model.where(name: mlb_team.division).first + unless division division = division_model.model.new(name: mlb_team.division, league: league) division.save! end - unless team = team_model.where(name: mlb_team.name).first + team = team_model.where(name: mlb_team.name).first + unless team team = team_model.model.new(name: mlb_team.name, logo_url: mlb_team.logo_url, manager: mlb_team.manager || 'None', ballpark: mlb_team.ballpark, mascot: mlb_team.mascot, founded: mlb_team.founded, wins: mlb_team.wins, losses: mlb_team.losses, win_percentage: format('%.3f', (mlb_team.wins.to_f / (mlb_team.wins + mlb_team.losses))).to_f, division: division) team.save! end diff --git a/spec/dummy_app/fly.toml b/spec/dummy_app/fly.toml new file mode 100644 index 0000000000..36c8da793e --- /dev/null +++ b/spec/dummy_app/fly.toml @@ -0,0 +1,22 @@ +# fly.toml app configuration file generated for rails-admin on 2023-08-06T18:22:31+09:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = "rails-admin" +primary_region = "iad" +console_command = "/rails/bin/rails console" + +[build] + +[http_service] + internal_port = 3000 + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 0 + processes = ["app"] + +[[statics]] + guest_path = "/rails/public" + url_prefix = "/" diff --git a/spec/dummy_app/lib/does_not_load_autoload_paths_not_in_eager_load.rb b/spec/dummy_app/lib/does_not_load_autoload_paths_not_in_eager_load.rb index 5481adc3f6..d744905c23 100644 --- a/spec/dummy_app/lib/does_not_load_autoload_paths_not_in_eager_load.rb +++ b/spec/dummy_app/lib/does_not_load_autoload_paths_not_in_eager_load.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module DoesNotLoadAutoloadPathsNotInEagerLoad raise 'This file is in app.paths.autoload but not app.paths.eager_load and ' \ ' should not be autoloaded by rails_admin' diff --git a/spec/dummy_app/package.json b/spec/dummy_app/package.json new file mode 100644 index 0000000000..4c879b9192 --- /dev/null +++ b/spec/dummy_app/package.json @@ -0,0 +1,25 @@ +{ + "name": "dummy_app", + "private": true, + "version": "0.1.0", + "dependencies": { + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@rails/actiontext": "^7.0.3-1", + "@rails/activestorage": "^7.0.3-1", + "@rails/webpacker": "5.4.3", + "rails_admin": "file:../..", + "trix": "^2.0.0-beta.0", + "webpack": "^4.46.0", + "webpack-cli": "^3.3.12" + }, + "devDependencies": { + "vite": "^5.0", + "vite-plugin-ruby": ">=5.0 <6", + "webpack-dev-server": "^3" + }, + "scripts": { + "build": "webpack --config webpack.config.js", + "build:css": "sass ./app/assets/stylesheets/rails_admin.scss:./app/assets/builds/rails_admin.css --no-source-map --load-path=node_modules" + } +} diff --git a/spec/dummy_app/postcss.config.js b/spec/dummy_app/postcss.config.js new file mode 100644 index 0000000000..37626ee68e --- /dev/null +++ b/spec/dummy_app/postcss.config.js @@ -0,0 +1,12 @@ +module.exports = { + plugins: [ + require("postcss-import"), + require("postcss-flexbugs-fixes"), + require("postcss-preset-env")({ + autoprefixer: { + flexbox: "no-2009", + }, + stage: 3, + }), + ], +}; diff --git a/spec/dummy_app/public/404.html b/spec/dummy_app/public/404.html index 9a48320a5f..39fbbdf9fc 100644 --- a/spec/dummy_app/public/404.html +++ b/spec/dummy_app/public/404.html @@ -1,26 +1,35 @@ - - The page you were looking for doesn't exist (404) - - + + The page you were looking for doesn't exist (404) + + - - -
    -

    The page you were looking for doesn't exist.

    -

    You may have mistyped the address or the page may have moved.

    -
    - + + +
    +

    The page you were looking for doesn't exist.

    +

    You may have mistyped the address or the page may have moved.

    +
    + diff --git a/spec/dummy_app/public/422.html b/spec/dummy_app/public/422.html index 83660ab187..1e44daec05 100644 --- a/spec/dummy_app/public/422.html +++ b/spec/dummy_app/public/422.html @@ -1,26 +1,35 @@ - - The change you wanted was rejected (422) - - + + The change you wanted was rejected (422) + + - - -
    -

    The change you wanted was rejected.

    -

    Maybe you tried to change something you didn't have access to.

    -
    - + + +
    +

    The change you wanted was rejected.

    +

    Maybe you tried to change something you didn't have access to.

    +
    + diff --git a/spec/dummy_app/public/500.html b/spec/dummy_app/public/500.html index f3648a0dbc..690421ed13 100644 --- a/spec/dummy_app/public/500.html +++ b/spec/dummy_app/public/500.html @@ -1,25 +1,34 @@ - - We're sorry, but something went wrong (500) - - + + We're sorry, but something went wrong (500) + + - - -
    -

    We're sorry, but something went wrong.

    -
    - + + +
    +

    We're sorry, but something went wrong.

    +
    + diff --git a/spec/dummy_app/vendor/javascript/.keep b/spec/dummy_app/vendor/javascript/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spec/dummy_app/vite.config.ts b/spec/dummy_app/vite.config.ts new file mode 100644 index 0000000000..ebd34ec97c --- /dev/null +++ b/spec/dummy_app/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vite"; +import RubyPlugin from "vite-plugin-ruby"; + +export default defineConfig({ + plugins: [RubyPlugin()], +}); diff --git a/spec/dummy_app/webpack.config.js b/spec/dummy_app/webpack.config.js new file mode 100644 index 0000000000..a72aced932 --- /dev/null +++ b/spec/dummy_app/webpack.config.js @@ -0,0 +1,21 @@ +const path = require("path"); +const webpack = require("webpack"); + +module.exports = { + mode: "production", + devtool: "source-map", + entry: { + application: "./app/javascript/application.js", + rails_admin: "./app/javascript/rails_admin.js", + }, + output: { + filename: "[name].js", + sourceMapFilename: "[name].js.map", + path: path.resolve(__dirname, "app/assets/builds"), + }, + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + ], +}; diff --git a/spec/factories.rb b/spec/factories.rb index 3a54674b79..0e1bbe8a32 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,4 +1,4 @@ -# coding: utf-8 +# frozen_string_literal: true FactoryBot.define do factory :player do @@ -27,6 +27,7 @@ sequence(:win_percentage) factory :managed_team, class: ManagedTeam + factory :restricted_team, class: RestrictedTeam end factory :league do @@ -42,6 +43,16 @@ sequence(:name) { |n| "Fan #{n}" } end + factory :fanship do + association :fan + association :team + end + + factory :favorite_player do + association :fanship + association :player + end + factory :user do sequence(:email) { |n| "username_#{n}@example.com" } sequence(:password) { |_n| 'password' } @@ -55,14 +66,14 @@ factory :comment do sequence(:content) do |n| - <<-EOF + <<-LOREM_IPSUM Lorém --#{n}-- ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." - EOF + LOREM_IPSUM end factory :comment_confirmed, class: Comment::Confirmed do @@ -71,7 +82,7 @@ end factory :ball do - color { %w(red blue green yellow purple brown black white).sample } + color { %w[red blue green yellow purple brown black white].sample } end factory :hardball do @@ -92,5 +103,13 @@ factory :paper_trail_test_subclass_in_namespace, parent: :paper_trail_test, class: 'PaperTrailTest::SubclassInNamespace' + + factory :paper_trail_test_with_custom_association, + parent: :paper_trail_test, + class: 'PaperTrailTestWithCustomAssociation' + end + + factory :two_level_namespaced_polymorphic_association_test, class: 'TwoLevel::Namespaced::PolymorphicAssociationTest' do + sequence(:name) { |n| "name #{n}" } end end diff --git a/spec/fixtures/test.txt b/spec/fixtures/test.txt new file mode 100644 index 0000000000..30d74d2584 --- /dev/null +++ b/spec/fixtures/test.txt @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/spec/helpers/rails_admin/application_helper_spec.rb b/spec/helpers/rails_admin/application_helper_spec.rb index 62c1eedc8c..fd49dd7a3b 100644 --- a/spec/helpers/rails_admin/application_helper_spec.rb +++ b/spec/helpers/rails_admin/application_helper_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::ApplicationHelper, type: :helper do @@ -70,10 +72,10 @@ it 'returns actions by type' do abstract_model = RailsAdmin::AbstractModel.new(Player) object = FactoryBot.create :player - expect(helper.actions(:all, abstract_model, object).collect(&:custom_key)).to eq([:dashboard, :index, :show, :new, :edit, :export, :delete, :bulk_delete, :history_show, :history_index, :show_in_app]) + expect(helper.actions(:all, abstract_model, object).collect(&:custom_key)).to eq(%i[dashboard index show new edit export delete bulk_delete history_show history_index show_in_app]) expect(helper.actions(:root, abstract_model, object).collect(&:custom_key)).to eq([:dashboard]) - expect(helper.actions(:collection, abstract_model, object).collect(&:custom_key)).to eq([:index, :new, :export, :bulk_delete, :history_index]) - expect(helper.actions(:member, abstract_model, object).collect(&:custom_key)).to eq([:show, :edit, :delete, :history_show, :show_in_app]) + expect(helper.actions(:collection, abstract_model, object).collect(&:custom_key)).to eq(%i[index new export bulk_delete history_index]) + expect(helper.actions(:member, abstract_model, object).collect(&:custom_key)).to eq(%i[show edit delete history_show show_in_app]) end it 'only returns visible actions, passing bindings correctly' do @@ -105,7 +107,7 @@ it 'uses first sign out method from Devise when it is defined' do allow(Object).to receive(:defined?).with(Devise).and_return(true) - expect(Devise).to receive(:sign_out_via).and_return([:whatever_defined_on_devise, :something_ignored]) + expect(Devise).to receive(:sign_out_via).and_return(%i[whatever_defined_on_devise something_ignored]) expect(helper.logout_method).to eq(:whatever_defined_on_devise) end end @@ -148,7 +150,7 @@ bc = helper.breadcrumb expect(bc).to match(/Dashboard/) # dashboard expect(bc).to match(/Teams/) # list - expect(bc).to match(/The avengers/) # show + expect(bc).to match(/the avengers/) # show expect(bc).to match(/Edit/) # current (edit) end end @@ -165,11 +167,11 @@ end show do visible do - bindings[:object].class == Team + bindings[:object].instance_of?(Team) end end delete do - http_methods [:post, :put, :delete] + http_methods %i[post put delete] end end end @@ -192,7 +194,7 @@ RailsAdmin.config do |config| config.actions do dashboard do - http_methods [:post, :put, :delete] + http_methods %i[post put delete] end end end @@ -223,6 +225,24 @@ expect(helper.menu_for(:root)).not_to match(/Dashboard/) expect(helper.menu_for(:root)).to match(/Look this/) end + + it 'should render allow an action to have link_target as config' do + RailsAdmin.config do |config| + config.actions do + dashboard + index + show do + link_target :_blank + end + end + end + + @action = RailsAdmin::Config::Actions.find :show + @abstract_model = RailsAdmin::AbstractModel.new(Team) + @object = FactoryBot.create(:team, name: 'the avengers') + + expect(helper.menu_for(:member, @abstract_model, @object)).to match(/_blank/) + end end describe '#main_navigation' do @@ -230,7 +250,7 @@ RailsAdmin.config do |config| config.included_models = [Ball, Comment] end - expect(helper.main_navigation).to match(/(dropdown-header).*(Navigation).*(Balls).*(Comments)/m) + expect(helper.main_navigation).to match(/(btn-toggle).*(Navigation).*(Balls).*(Comments)/m) end it 'does not draw empty navigation labels' do @@ -243,8 +263,8 @@ label_plural 'Confirmed' end end - expect(helper.main_navigation).to match(/(dropdown-header).*(Navigation).*(Balls).*(Commentz).*(Confirmed)/m) - expect(helper.main_navigation).not_to match(/(dropdown-header).*(Navigation).*(Balls).*(Commentz).*(Confirmed).*(Comment)/m) + expect(helper.main_navigation).to match(/(btn-toggle).*(Navigation).*(Balls).*(Commentz).*(Confirmed)/m) + expect(helper.main_navigation).not_to match(/(btn-toggle).*(Navigation).*(Balls).*(Commentz).*(Confirmed).*(Comment)/m) end it 'does not show unvisible models' do @@ -255,25 +275,25 @@ end end result = helper.main_navigation - expect(result).to match(/(dropdown-header).*(Navigation).*(Balls)/m) + expect(result).to match(/(btn-toggle).*(Navigation).*(Balls)/m) expect(result).not_to match('Comments') end - it 'shows children of hidden models' do # https://github.com/sferik/rails_admin/issues/978 + it 'shows children of hidden models' do # https://github.com/railsadminteam/rails_admin/issues/978 RailsAdmin.config do |config| config.included_models = [Ball, Hardball] config.model Ball do hide end end - expect(helper.main_navigation).to match(/(dropdown\-header).*(Navigation).*(Hardballs)/m) + expect(helper.main_navigation).to match(/(btn-toggle).*(Navigation).*(Hardballs)/m) end it 'shows children of excluded models' do RailsAdmin.config do |config| config.included_models = [Hardball] end - expect(helper.main_navigation).to match(/(dropdown-header).*(Navigation).*(Hardballs)/m) + expect(helper.main_navigation).to match(/(btn-toggle).*(Navigation).*(Hardballs)/m) end it 'nests in navigation label' do @@ -283,7 +303,7 @@ navigation_label 'commentable' end end - expect(helper.main_navigation).to match(/(dropdown\-header).*(Commentable).*(Comments)/m) + expect(helper.main_navigation).to match(/(btn-toggle).*(commentable).*(Comments)/m) end it 'nests in parent model' do @@ -293,7 +313,7 @@ parent Player end end - expect(helper.main_navigation).to match(/(Players).* (nav\-level\-1).*(Comments)/m) + expect(helper.main_navigation).to match(/(Players).* (nav-level-1).*(Comments)/m) end it 'orders' do @@ -443,32 +463,55 @@ end describe '#edit_user_link' do - it "don't include email column" do - allow(helper).to receive(:_current_user).and_return(FactoryBot.create(:player)) - result = helper.edit_user_link - expect(result).to eq nil + subject { helper.edit_user_link } + let(:user) { FactoryBot.create(:user) } + before { allow(helper).to receive(:_current_user).and_return(user) } + + it 'shows the edit action link of the user' do + is_expected.to match(%r{href="[^"]+/admin/user/#{user.id}/edit"}) end - it 'include email column' do - allow(helper).to receive(:_current_user).and_return(FactoryBot.create(:user)) - result = helper.edit_user_link - expect(result).to match('href') + it 'shows the gravatar icon' do + is_expected.to include('gravatar') end - it 'show gravatar' do - allow(helper).to receive(:_current_user).and_return(FactoryBot.create(:user)) - result = helper.edit_user_link - expect(result).to include('gravatar') + context "when the user doesn't have the email column" do + let(:user) { FactoryBot.create(:player) } + + it 'shows nothing' do + is_expected.to be nil + end end - it "don't show gravatar" do - RailsAdmin.config do |config| - config.show_gravatar = false + context 'when gravatar is disabled' do + before { RailsAdmin.config.show_gravatar = false } + + it "doesn't show the gravatar icon" do + is_expected.not_to include('gravatar') + end + end + + context 'when the user is not authorized to perform edit' do + before do + allow_any_instance_of(RailsAdmin::Config::Actions::Edit).to receive(:authorized?).and_return(false) + end + + it 'shows gravatar and email without a link' do + is_expected.to include('gravatar') + is_expected.to include(user.email) + is_expected.not_to match('href') end - allow(helper).to receive(:_current_user).and_return(FactoryBot.create(:user)) - result = helper.edit_user_link - expect(result).not_to include('gravatar') + it 'shows only email without a link when gravatar is disabled' do + RailsAdmin.config do |config| + config.show_gravatar = false + end + + is_expected.not_to include('gravatar') + is_expected.not_to match('href') + is_expected.to include(user.email) + is_expected.to match("#{user.email}") + end end end end diff --git a/spec/helpers/rails_admin/form_builder_spec.rb b/spec/helpers/rails_admin/form_builder_spec.rb index e78ba6254c..86d0dd205d 100644 --- a/spec/helpers/rails_admin/form_builder_spec.rb +++ b/spec/helpers/rails_admin/form_builder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'RailsAdmin::FormBuilder', type: :helper do @@ -13,6 +15,9 @@ (@object = Player.new).save @builder = RailsAdmin::FormBuilder.new(:player, @object, helper, {}) allow(@builder).to receive(:field_for).and_return('field') + action = double + allow(action).to receive(:enabled?).and_return true + helper.instance_variable_set :@action, action end it 'does not add additional error div from default ActionView::Base.field_error_proc' do @@ -21,7 +26,7 @@ end it 'hidden fields should be wrapper' do - expect(@builder.generate(action: :create, model_config: RailsAdmin.config(Player))).to match('form-group control-group hidden_type number_field') + expect(@builder.generate(action: :create, model_config: RailsAdmin.config(Player))).to match('control-group row mb-3 hidden_type number_field') end end diff --git a/spec/helpers/rails_admin/main_helper_spec.rb b/spec/helpers/rails_admin/main_helper_spec.rb index 670e5442b7..358cd81615 100644 --- a/spec/helpers/rails_admin/main_helper_spec.rb +++ b/spec/helpers/rails_admin/main_helper_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::MainHelper, type: :helper do @@ -36,11 +38,11 @@ end it 'should add novalidate attribute to the html form tag' do - expect(html_form).to include "novalidate=\"novalidate\"" + expect(html_form).to include 'novalidate="novalidate"' end it 'should add novalidate attribute to the html form tag with html attributes' do - expect(html_form_with_attrs).to include "novalidate=\"novalidate\"" + expect(html_form_with_attrs).to include 'novalidate="novalidate"' end end end diff --git a/spec/integration/actions/base_spec.rb b/spec/integration/actions/base_spec.rb index f677d2e8b9..b0f44f4b8d 100644 --- a/spec/integration/actions/base_spec.rb +++ b/spec/integration/actions/base_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'Base action', type: :request do @@ -8,7 +10,7 @@ RailsAdmin.config do |config| config.actions do index do - except %w(FieldTest) + except %w[FieldTest] end end end @@ -22,7 +24,7 @@ index new edit do - except %w(Player Team) + except %w[Player Team] end end end @@ -44,5 +46,28 @@ end end end + + context 'when used with #visible?' do + let!(:player) { FactoryBot.create(:player) } + before do + RailsAdmin.config do |config| + config.actions do + index + show + edit do + enabled false + visible true + end + end + end + end + + it 'allows disabled links to be shown' do + visit index_path(model_name: 'player') + is_expected.to have_css('.edit_member_link.disabled span', text: /Edit/, visible: false) + visit show_path(model_name: 'player', id: player.id) + is_expected.to have_css('.edit_member_link.disabled a[href*="void(0)"]') + end + end end end diff --git a/spec/integration/actions/bulk_delete_spec.rb b/spec/integration/actions/bulk_delete_spec.rb index 4349dbeb3d..43a1af59bd 100644 --- a/spec/integration/actions/bulk_delete_spec.rb +++ b/spec/integration/actions/bulk_delete_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'BulkDelete action', type: :request do @@ -49,8 +51,6 @@ context 'on destroy' do before do - RailsAdmin::History.destroy_all - RailsAdmin.config { |c| c.audit_with :history } @players = Array.new(3) { FactoryBot.create(:player) } @delete_ids = @players[0..1].collect(&:id) @@ -63,15 +63,8 @@ click_button "Yes, I'm sure" end - it 'does not contain deleted records', active_record: true do - expect(RailsAdmin::AbstractModel.new('Player').count).to eq(1) - expect(RailsAdmin::History.count).to eq(@delete_ids.count) - RailsAdmin::History.all.each do |history| - expect(history.table).to eq('Player') - end - RailsAdmin::History.all.each do |history| - expect(@delete_ids).to include(history.item) - end + it 'does not contain deleted records' do + expect(RailsAdmin::AbstractModel.new('Player').all.pluck(:id)).to eq([@players[2].id]) expect(page).to have_selector('.alert-success', text: '2 Players successfully deleted') end end @@ -81,17 +74,31 @@ @players = Array.new(3) { FactoryBot.create(:player) } @delete_ids = @players[0..1].collect(&:id) - # NOTE: This uses an internal, unsupported capybara API which could break at any moment. We - # should refactor this test so that it either A) uses capybara's supported API (only GET - # requests via visit) or B) just uses Rack::Test (and doesn't use capybara for browser - # interaction like click_button). - page.driver.browser.reset_host! - page.driver.browser.process :post, bulk_action_path(bulk_action: 'bulk_delete', model_name: 'player', bulk_ids: @delete_ids, '_method' => 'post') - click_button 'Cancel' + visit index_path(model_name: 'player') + @delete_ids.each { |id| find(%(input[name="bulk_ids[]"][value="#{id}"])).click } + click_link 'Selected items' + click_link 'Delete selected Players' end - it 'does not delete records' do + it 'does not delete records', js: true do + find_button('Cancel').trigger('click') + is_expected.to have_text 'No actions were taken' expect(RailsAdmin::AbstractModel.new('Player').count).to eq(3) end end + + context 'with composite primary keys', composite_primary_keys: true do + let!(:fanships) { FactoryBot.create_list(:fanship, 3) } + + it 'provides check boxes for bulk operation' do + visit index_path(model_name: 'fanship') + fanships.each { |fanship| is_expected.to have_css(%(input[name="bulk_ids[]"][value="#{fanship.id}"])) } + end + + it 'deletes selected records' do + delete(bulk_delete_path(bulk_action: 'bulk_delete', model_name: 'fanship', bulk_ids: fanships[0..1].map { |fanship| RailsAdmin::Support::CompositeKeysSerializer.serialize(fanship.id) })) + expect(flash[:success]).to match(/2 Fanships successfully deleted/) + expect(Fanship.all).to eq fanships[2..2] + end + end end diff --git a/spec/integration/actions/dashboard_spec.rb b/spec/integration/actions/dashboard_spec.rb index 2f7c572b92..260f086683 100644 --- a/spec/integration/actions/dashboard_spec.rb +++ b/spec/integration/actions/dashboard_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'Dashboard action', type: :request do @@ -24,6 +26,28 @@ visit dashboard_path end + it 'does not show history if turned off', active_record: true do + RailsAdmin.config do |c| + c.audit_with :paper_trail, 'User', 'PaperTrail::Version' + c.included_models = [PaperTrailTest] + c.actions do + dashboard do + history false + end + index # mandatory + new + history_index + end + end + with_versioning do + visit new_path(model_name: 'paper_trail_test') + fill_in 'paper_trail_test[name]', with: 'Jackie Robinson' + click_button 'Save' + end + visit dashboard_path + is_expected.not_to have_content 'Jackie Robinson' + end + it 'counts are different for same-named models in different modules' do allow(RailsAdmin.config(User::Confirmed).abstract_model).to receive(:count).and_return(10) allow(RailsAdmin.config(Comment::Confirmed).abstract_model).to receive(:count).and_return(0) diff --git a/spec/integration/actions/delete_spec.rb b/spec/integration/actions/delete_spec.rb index 19a599e98d..fc69f9fb92 100644 --- a/spec/integration/actions/delete_spec.rb +++ b/spec/integration/actions/delete_spec.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'Delete action', type: :request do subject { page } - it "shows \"Delete model\"" do + it 'shows "Delete model"' do @draft = FactoryBot.create :draft @player = @draft.player @comment = @player.comments.create @@ -37,7 +39,7 @@ visit delete_path(model_name: 'player', id: @player.id) end - it "shows \"Delete model\"" do + it 'shows "Delete model"' do is_expected.to have_content('delete this player') is_expected.not_to have_selector("a[href=\"/admin/player/#{@player.id}\"]") is_expected.not_to have_selector("a[href=\"/admin/draft/#{@draft.id}\"]") @@ -52,7 +54,7 @@ visit delete_path(model_name: 'player', id: @player.id) end - it "shows \"Delete model\"" do + it 'shows "Delete model"' do is_expected.not_to have_content('Routing Error') is_expected.to have_content('delete this player') is_expected.to have_link(@player.name, href: "/admin/player/#{@player.id}") @@ -105,8 +107,25 @@ is_expected.to have_content('Player failed to be deleted') end - it 'returns status code 200' do - expect(page.status_code).to eq(200) + it 'returns status code 406' do + expect(page.status_code).to eq(406) + end + end + + context 'on destroy error by dependent: :restrict_with_error' do + let!(:player) { FactoryBot.create :player, team: FactoryBot.create(:restricted_team) } + before do + visit delete_path(model_name: 'restricted_team', id: player.team.id) + click_button "Yes, I'm sure" + is_expected.to have_content('Restricted team failed to be deleted') + end + + it 'shows error message', active_record: true do + is_expected.to have_content('Cannot delete record because dependent players exist') + end + + it 'shows error message', mongoid: true do + is_expected.to have_content('Players is not empty and prevents the document from being destroyed') end end @@ -114,12 +133,12 @@ before do @player = FactoryBot.create :player visit delete_path(model_name: 'player', id: @player.id) - click_button 'Cancel' - @player = RailsAdmin::AbstractModel.new('Player').first end - it 'does not destroy an object' do - expect(@player).to be + it 'does not destroy an object', js: true do + find_button('Cancel').trigger('click') + is_expected.to have_text 'No actions were taken' + expect(RailsAdmin::AbstractModel.new('Player').first).to be end end @@ -143,26 +162,25 @@ expect(URI.parse(page.current_url).path).to eq(index_path(model_name: 'player')) end - it 'redirects back to the object on error' do + it 'stays on the delete page' do allow_any_instance_of(Player).to receive(:destroy_hook) { throw :abort } @player = FactoryBot.create :player visit show_path(model_name: 'player', id: @player.id) click_link 'Delete' click_button "Yes, I'm sure" - expect(URI.parse(page.current_url).path).to eq(show_path(model_name: 'player', id: @player.id)) + expect(URI.parse(page.current_url).path).to eq(delete_path(model_name: 'player', id: @player.id)) end end - context 'when navigated to delete from index page' do - it 'returns status code 200' do - allow_any_instance_of(Player).to receive(:destroy_hook) { throw :abort } - @player = FactoryBot.create :player - visit index_path(model_name: 'player') - click_link 'Delete' - click_button "Yes, I'm sure" + context 'with composite primary keys', composite_primary_keys: true do + let(:fanship) { FactoryBot.create(:fanship) } - expect(page.status_code).to eq(200) + it 'deletes the object' do + visit delete_path(model_name: 'fanship', id: fanship.id) + click_button "Yes, I'm sure" + is_expected.to have_content('Fanship successfully deleted') + expect(Fanship.all).to be_empty end end end diff --git a/spec/integration/actions/edit_spec.rb b/spec/integration/actions/edit_spec.rb index ed47d8db17..6fa5d1b323 100644 --- a/spec/integration/actions/edit_spec.rb +++ b/spec/integration/actions/edit_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'Edit action', type: :request do @@ -9,19 +11,19 @@ visit edit_path(model_name: 'player', id: @player.id) end - it "shows \"Edit model\"" do + it 'shows "Edit model"' do is_expected.to have_content('Edit Player') end - it "shows required fields as \"Required\"" do + it 'shows required fields as "Required"' do is_expected.to have_selector('div', text: /Name\s*Required/) is_expected.to have_selector('div', text: /Number\s*Required/) end - it "shows non-required fields as \"Optional\"" do - expect(find('#player_position_field .help-block')).to have_content('Optional') - expect(find('#player_born_on_field .help-block')).to have_content('Optional') - expect(find('#player_notes_field .help-block')).to have_content('Optional') + it 'shows non-required fields as "Optional"' do + expect(find('#player_position_field .form-text')).to have_content('Optional') + expect(find('#player_born_on_field .form-text')).to have_content('Optional') + expect(find('#player_notes_field .form-text')).to have_content('Optional') end it 'checks required fields to have required attribute set' do @@ -118,19 +120,19 @@ class HelpTest < Tableless context 'using mongoid', skip_active_record: true do it 'uses the db column size for the maximum length' do visit new_path(model_name: 'help_test') - expect(find('#help_test_name_field .help-block')).to have_content('Length up to 255.') + expect(find('#help_test_name_field .form-text')).to have_content('Length up to 255.') end it 'returns nil for the maximum length' do visit new_path(model_name: 'team') - expect(find('#team_custom_field_field .help-block')).not_to have_content('Length') + expect(find('#team_custom_field_field .form-text')).not_to have_content('Length') end end context 'using active_record', skip_mongoid: true do it 'uses the db column size for the maximum length' do visit new_path(model_name: 'help_test') - expect(find('#help_test_name_field .help-block')).to have_content('Length up to 50.') + expect(find('#help_test_name_field .form-text')).to have_content('Length up to 50.') end it 'uses the :minimum setting from the validation' do @@ -138,7 +140,7 @@ class HelpTest < Tableless validates_length_of :name, minimum: 1 end visit new_path(model_name: 'help_test') - expect(find('#help_test_name_field .help-block')).to have_content('Length of 1-50.') + expect(find('#help_test_name_field .form-text')).to have_content('Length of 1-50.') end it 'uses the minimum of db column size or :maximum setting from the validation' do @@ -146,7 +148,7 @@ class HelpTest < Tableless validates_length_of :name, maximum: 51 end visit new_path(model_name: 'help_test') - expect(find('#help_test_name_field .help-block')).to have_content('Length up to 50.') + expect(find('#help_test_name_field .form-text')).to have_content('Length up to 50.') end end @@ -199,7 +201,7 @@ class HelpTest < Tableless validates_length_of :name, is: 3 end visit new_path(model_name: 'help_test') - expect(find('#help_test_name_field .help-block')).to have_content('Length of 3.') + expect(find('#help_test_name_field .form-text')).to have_content('Length of 3.') end it 'uses the :maximum setting from the validation' do @@ -207,7 +209,7 @@ class HelpTest < Tableless validates_length_of :name, maximum: 49 end visit new_path(model_name: 'help_test') - expect(find('#help_test_name_field .help-block')).to have_content('Length up to 49.') + expect(find('#help_test_name_field .form-text')).to have_content('Length up to 49.') end it 'uses the :minimum and :maximum from the validation' do @@ -215,7 +217,7 @@ class HelpTest < Tableless validates_length_of :name, minimum: 1, maximum: 49 end visit new_path(model_name: 'help_test') - expect(find('#help_test_name_field .help-block')).to have_content('Length of 1-49.') + expect(find('#help_test_name_field .form-text')).to have_content('Length of 1-49.') end it 'uses the range from the validation' do @@ -223,7 +225,7 @@ class HelpTest < Tableless validates_length_of :name, in: 1..49 end visit new_path(model_name: 'help_test') - expect(find('#help_test_name_field .help-block')).to have_content('Length of 1-49.') + expect(find('#help_test_name_field .form-text')).to have_content('Length of 1-49.') end it 'does not show help for hidden fields' do @@ -233,7 +235,7 @@ class HelpTest < Tableless end end visit new_path(model_name: 'help_test') - expect(page).not_to have_css('.help-block') + expect(page).not_to have_css('.form-text') end end @@ -289,7 +291,7 @@ class HelpTest < Tableless end end - describe "fields" do + describe 'fields' do it 'shows all by default' do visit new_path(model_name: 'team') is_expected.to have_selector('select#team_division_id') @@ -426,13 +428,13 @@ class HelpTest < Tableless field :name do read_only true formatted_value do - "I'm outputed in the form" + "I'm outputted in the form" end end end end visit new_path(model_name: 'team') - is_expected.to have_content("I'm outputed in the form") + is_expected.to have_content("I'm outputted in the form") end it 'is hideable' do @@ -510,9 +512,9 @@ class HelpTest < Tableless end end visit new_path(model_name: 'team') - expect(find('#team_manager_field .help-block')).to have_content('Required. Length up to 100. Additional help text for manager field.') - expect(find('#team_division_id_field .help-block')).to have_content('Required') - expect(find('#team_name_field .help-block')).not_to have_content('Additional help text') + expect(find('#team_manager_field .form-text')).to have_content('Required. Length up to 100. Additional help text for manager field.') + expect(find('#team_division_id_field .form-text')).to have_content('Required') + expect(find('#team_name_field .form-text')).not_to have_content('Additional help text') end it 'has option to override required status' do @@ -530,9 +532,9 @@ class HelpTest < Tableless end end visit new_path(model_name: 'team') - expect(find('#team_manager_field .help-block')).to have_content('Optional') - expect(find('#team_division_id_field .help-block')).to have_content('Optional') - expect(find('#team_name_field .help-block')).to have_content(I18n.translate('admin.help.team.name')) + expect(find('#team_manager_field .form-text')).to have_content('Optional') + expect(find('#team_division_id_field .form-text')).to have_content('Optional') + expect(find('#team_name_field .form-text')).to have_content(I18n.translate('admin.help.team.name')) end describe 'inline_add' do @@ -659,6 +661,14 @@ class HelpTest < Tableless end end + context 'with a readonly object' do + let(:comment) { FactoryBot.create :comment, (CI_ORM == :mongoid ? {_type: 'ReadOnlyComment'} : {}) } + + it 'raises ActionNotAllowed' do + expect { visit edit_path(model_name: 'read_only_comment', id: comment.id) }.to raise_error 'RailsAdmin::ActionNotAllowed' + end + end + context 'with missing label', given: ['a player exists', 'three teams with no name exist'] do before do @player = FactoryBot.create :player @@ -680,51 +690,21 @@ class HelpTest < Tableless context 'on cancel' do before do - @ball = FactoryBot.create :ball - visit '/admin/ball?sort=color' + @player = FactoryBot.create :player + visit '/admin/player' click_link 'Edit' end - it "shows cancel button with 'novalidate' attribute" do - expect(page).to have_css '[type="submit"][name="_continue"][formnovalidate]' - end - - it 'sends back to previous URL' do - click_button 'Cancel' - expect(page.current_url).to eq('http://www.example.com/admin/ball?sort=color') + it 'sends back to previous URL', js: true do + find_button('Cancel').trigger('click') + is_expected.to have_text 'No actions were taken' + expect(page.current_path).to eq('/admin/player') end - end - - context 'on clicking save without changing anything' do - before { @datetime = 'October 08, 2015 06:45' } - context 'when config.time_zone set' do - before do - RailsAdmin.config Player do - field :datetime_field - end - @old_timezone = Time.zone - Time.zone = ActiveSupport::TimeZone.new('Central Time (US & Canada)') - end - - after do - Time.zone = @old_timezone - end - it 'does not alter datetime fields' do - visit new_path(model_name: 'field_test') - find('#field_test_datetime_field').set(@datetime) - click_button 'Save and edit' - expect(find('#field_test_datetime_field').value).to eq(@datetime) - end - end - - context 'without config.time_zone set (default)' do - it 'does not alter datetime fields' do - visit new_path(model_name: 'field_test') - find('#field_test_datetime_field').set(@datetime) - click_button 'Save and edit' - expect(find('#field_test_datetime_field').value).to eq(@datetime) - end + it 'allows submit even if client-side validation is not satisfied', js: true do + fill_in 'player[name]', with: '' + find_button('Cancel').trigger('click') + is_expected.to have_text 'No actions were taken' end end @@ -763,7 +743,7 @@ class HelpTest < Tableless end end - describe 'update and edit' do + describe 'update and edit', js: true do before do @player = FactoryBot.create :player @@ -772,18 +752,37 @@ class HelpTest < Tableless fill_in 'player[name]', with: 'Jackie Robinson' fill_in 'player[number]', with: '42' fill_in 'player[position]', with: 'Second baseman' - click_button 'Save and edit' - - @player.reload + find_button('Save and edit').trigger('click') end it 'updates an object with correct attributes' do + is_expected.to have_text 'Player successfully updated' + expect(page.current_path).to eq("/admin/player/#{@player.id}/edit") + + @player.reload expect(@player.name).to eq('Jackie Robinson') expect(@player.number).to eq(42) expect(@player.position).to eq('Second baseman') end end + context 'with a submit button with custom value', js: true do + before do + @player = FactoryBot.create :player + + visit edit_path(model_name: 'player', id: @player.id) + + execute_script %{$('.form-actions [name="_save"]').attr('name', 'player[name]').attr('value', 'Jackie Robinson')} + find_button('Save').trigger('click') + is_expected.to have_text 'Player successfully updated' + end + + it 'submits the value' do + @player.reload + expect(@player.name).to eq('Jackie Robinson') + end + end + context 'with missing object' do before do put edit_path(model_name: 'player', id: 1), params: {player: {name: 'Jackie Robinson', number: 42, position: 'Second baseman'}} @@ -861,4 +860,62 @@ class HelpTest < Tableless expect(@record.format).to eq('test for format') end end + + context "with a field with 'open' as a name" do + it 'is updatable without any error' do + RailsAdmin.config FieldTest do + edit do + field :open do + nullable false + end + end + end + record = FieldTest.create + visit edit_path(model_name: 'field_test', id: record.id) + expect do + check 'field_test[open]' + click_button 'Save' + end.to change { record.reload.open }.from(nil).to(true) + end + end + + context 'with composite primary keys', composite_primary_keys: true do + let(:fanship) { FactoryBot.create(:fanship) } + + it 'edits the object' do + visit edit_path(model_name: 'fanship', id: fanship.id) + fill_in 'Since', with: '2000-01-23' + click_button 'Save' + expect { fanship.reload }.to change { fanship.since }.from(nil).to(Date.new(2000, 1, 23)) + end + + context 'using custom serializer' do + before do + RailsAdmin.config.composite_keys_serializer = Class.new do + def self.serialize(keys) + keys.join(',') + end + + def self.deserialize(string) + string.split(',') + end + end + end + + it 'edits the object' do + visit edit_path(model_name: 'fanship', id: "#{fanship.fan_id},#{fanship.team_id}") + fill_in 'Since', with: '2000-01-23' + click_button 'Save' + expect { fanship.reload }.to change { fanship.since }.from(nil).to(Date.new(2000, 1, 23)) + end + end + + context 'receiving invalid id' do + it 'returns 404' do + visit edit_path(model_name: 'fanship', id: '11') + expect(page.driver.status_code).to eq(404) + is_expected.to have_content("Fanship with id '11' could not be found") + end + end + end end diff --git a/spec/integration/actions/export_spec.rb b/spec/integration/actions/export_spec.rb index 9e1db15152..4ba5ea8f40 100644 --- a/spec/integration/actions/export_spec.rb +++ b/spec/integration/actions/export_spec.rb @@ -1,108 +1,173 @@ +# frozen_string_literal: true + require 'spec_helper' require 'csv' RSpec.describe 'Export action', type: :request do subject { page } - before do - Comment.all.collect(&:destroy) # rspec bug => doesn't get destroyed with transaction - - @players = FactoryBot.create_list(:player, 4) - @player = @players.first - @player.team = FactoryBot.create :team - @player.draft = FactoryBot.create :draft - @player.comments = (@comments = Array.new(2) { FactoryBot.create(:comment) }) - @player.save - - @abstract_model = RailsAdmin::AbstractModel.new(Player) - - # removed schema=>only=>created_at - @non_default_schema = { - 'only' => [PK_COLUMN.to_s, 'updated_at', 'deleted_at', 'name', 'position', 'number', 'retired', 'injured', 'born_on', 'notes', 'suspended'], - 'include' => { - 'team' => {'only' => [PK_COLUMN.to_s, 'created_at', 'updated_at', 'name', 'logo_url', 'manager', 'ballpark', 'mascot', 'founded', 'wins', 'losses', 'win_percentage', 'revenue', 'color']}, - 'draft' => {'only' => [PK_COLUMN.to_s, 'created_at', 'updated_at', 'date', 'round', 'pick', 'overall', 'college', 'notes']}, - 'comments' => {'only' => [PK_COLUMN.to_s, 'content', 'created_at', 'updated_at']}, - }, - } - end - - it 'allows to export to CSV with associations and default schema, containing properly translated header and follow configuration' do - RailsAdmin.config do |c| - c.model Player do - include_all_fields - field :name do - export_value do - "#{value} exported" - end - end - - field :json_field, :json do - formatted_value do - '{}' - end - end - end - end + let!(:player) { FactoryBot.create(:player) } + it 'exports to CSV' do visit export_path(model_name: 'player') - is_expected.to have_content 'Select fields to export' - select " ','", from: 'csv_options_generator_col_sep' click_button 'Export to csv' - csv = CSV.parse page.driver.response.body.force_encoding('utf-8') # comes through as us-ascii on some platforms - expect(csv[0]).to match_array ['Id', 'Created at', 'Updated at', 'Deleted at', 'Name', 'Position', - 'Number', 'Retired', 'Injured', 'Born on', 'Notes', 'Suspended', 'Formation', 'Json field', 'Id [Team]', 'Created at [Team]', - 'Updated at [Team]', 'Name [Team]', 'Logo url [Team]', 'Team Manager [Team]', 'Ballpark [Team]', - 'Mascot [Team]', 'Founded [Team]', 'Wins [Team]', 'Losses [Team]', 'Win percentage [Team]', - 'Revenue [Team]', 'Color [Team]', 'Custom field [Team]', 'Main Sponsor [Team]', 'Id [Draft]', 'Created at [Draft]', - 'Updated at [Draft]', 'Date [Draft]', 'Round [Draft]', 'Pick [Draft]', 'Overall [Draft]', - 'College [Draft]', 'Notes [Draft]', 'Id [Comments]', 'Content [Comments]', 'Created at [Comments]', - 'Updated at [Comments]'] - expect(csv.flatten).to include(@player.name + ' exported') - expect(csv.flatten).to include(@player.team.name) - expect(csv.flatten).to include(@player.draft.college) - - expect(csv.flatten.join(' ')).to include(@player.comments.first.content.split("\n").first.strip) - expect(csv.flatten.join(' ')).to include(@player.comments.second.content.split("\n").first.strip) + is_expected.to have_content player.name end - it 'allows to export to JSON' do + it 'exports to JSON' do visit export_path(model_name: 'player') click_button 'Export to json' - is_expected.to have_content @player.team.name + is_expected.to have_content player.name end - it 'allows to export to XML' do + it 'exports to XML' do pending "Mongoid does not support to_xml's :include option" if CI_ORM == :mongoid visit export_path(model_name: 'player') click_button 'Export to xml' - is_expected.to have_content @player.team.name + is_expected.to have_content player.name + end + + it 'works with Turbo Drive enabled', js: true do + visit export_path(model_name: 'player') + page.execute_script 'console.error = function(error) { throw error }' + expect { find_button('Export to csv').trigger('click') }.not_to raise_error end - it 'exports polymorphic fields the easy way for now' do + it 'does not break when nothing is checked' do visit export_path(model_name: 'comment') - select " ','", from: 'csv_options_generator_col_sep' - click_button 'Export to csv' - csv = CSV.parse page.driver.response.body - expect(csv[0]).to match_array ['Id', 'Commentable', 'Commentable type', 'Content', 'Created at', 'Updated at'] - csv[1..-1].each do |line| - expect(line[csv[0].index('Commentable')]).to eq(@player.id.to_s) - expect(line[csv[0].index('Commentable type')]).to eq(@player.class.to_s) - end + all('input[type="checkbox"]').each(&:uncheck) + expect { click_button 'Export to csv' }.not_to raise_error end - context 'with csv format' do - it 'exports with modified schema' do - page.driver.post(export_path(model_name: 'player', schema: @non_default_schema, csv: true, all: true, csv_options: {generator: {col_sep: ','}})) + describe 'with associations' do + let!(:players) { FactoryBot.create_list(:player, 3) } + let(:team) { FactoryBot.create :team } + let(:draft) { FactoryBot.create :draft } + let(:comments) { FactoryBot.create_list(:comment, 2) } + + before do + player.team = team + player.draft = draft + player.comments = comments + player.save + end + + it 'exports to CSV with default schema, containing properly translated header and follow configuration' do + RailsAdmin.config do |c| + c.model Player do + include_all_fields + field :name do + export_value do + "#{value} exported" + end + end + + field :json_field, :json do + formatted_value do + '{}' + end + end + end + end + + visit export_path(model_name: 'player') + is_expected.to have_content 'Select fields to export' + select " ','", from: 'csv_options_generator_col_sep' + click_button 'Export to csv' + csv = CSV.parse page.driver.response.body.force_encoding('utf-8') # comes through as us-ascii on some platforms + expect(csv[0]).to match_array ['Id', 'Created at', 'Updated at', 'Deleted at', 'Name', 'Position', + 'Number', 'Retired', 'Injured', 'Born on', 'Notes', 'Suspended', 'Formation', 'Json field', 'Id [Team]', 'Created at [Team]', + 'Updated at [Team]', 'Name [Team]', 'Logo url [Team]', 'Team Manager [Team]', 'Ballpark [Team]', + 'Mascot [Team]', 'Founded [Team]', 'Wins [Team]', 'Losses [Team]', 'Win percentage [Team]', + 'Revenue [Team]', 'Color [Team]', 'Custom field [Team]', 'Main Sponsor [Team]', 'Id [Draft]', 'Created at [Draft]', + 'Updated at [Draft]', 'Date [Draft]', 'Round [Draft]', 'Pick [Draft]', 'Overall [Draft]', + 'College [Draft]', 'Notes [Draft]', 'Id [Comments]', 'Content [Comments]', 'Created at [Comments]', + 'Updated at [Comments]'] + expect(csv.flatten).to include("#{player.name} exported") + expect(csv.flatten).to include(player.team.name) + expect(csv.flatten).to include(player.draft.college) + + expect(csv.flatten.join(' ')).to include(player.comments.first.content.split("\n").first.strip) + expect(csv.flatten.join(' ')).to include(player.comments.second.content.split("\n").first.strip) + end + + let(:custom_schema) do + # removed schema=>only=>created_at + { + 'only' => [PK_COLUMN.to_s, 'updated_at', 'deleted_at', 'name', 'position', 'number', 'retired', 'injured', 'born_on', 'notes', 'suspended'], + 'include' => { + 'team' => {'only' => [PK_COLUMN.to_s, 'created_at', 'updated_at', 'name', 'logo_url', 'manager', 'ballpark', 'mascot', 'founded', 'wins', 'losses', 'win_percentage', 'revenue', 'color']}, + 'draft' => {'only' => [PK_COLUMN.to_s, 'created_at', 'updated_at', 'date', 'round', 'pick', 'overall', 'college', 'notes']}, + 'comments' => {'only' => [PK_COLUMN.to_s, 'content', 'created_at', 'updated_at']}, + }, + } + end + + it 'exports to CSV with custom schema' do + page.driver.post(export_path(model_name: 'player', schema: custom_schema, csv: true, all: true, csv_options: {generator: {col_sep: ','}})) csv = CSV.parse page.driver.response.body expect(csv[0]).not_to include('Created at') end + + it 'exports polymorphic fields the easy way for now' do + visit export_path(model_name: 'comment') + select " ','", from: 'csv_options_generator_col_sep' + click_button 'Export to csv' + csv = CSV.parse page.driver.response.body + expect(csv[0]).to match_array ['Id', 'Commentable', 'Commentable type', 'Content', 'Created at', 'Updated at'] + csv[1..].each do |line| + expect(line[csv[0].index('Commentable')]).to eq(player.id.to_s) + expect(line[csv[0].index('Commentable type')]).to eq(player.class.to_s) + end + end + end + + context 'on cancel' do + before do + @player = FactoryBot.create :player + visit export_path(model_name: 'player') + end + + it 'does nothing', js: true do + find_button('Cancel').trigger('click') + is_expected.to have_text 'No actions were taken' + end end - it 'supports bulk export' do - visit index_path(model_name: 'player') - click_link 'Export found Players' - is_expected.to have_content('Select fields to export') + describe 'bulk export' do + it 'is supported' do + visit index_path(model_name: 'player') + click_link 'Export found Players' + is_expected.to have_content('Select fields to export') + end + + describe 'with model scope' do + let!(:comments) { %w[something anything].map { |content| FactoryBot.create :comment_confirmed, content: content } } + before do + RailsAdmin.config do |config| + config.model Comment::Confirmed do + scope { Comment::Confirmed.unscoped } + end + end + end + + it 'overrides default_scope' do + page.driver.post(export_path(model_name: 'comment~confirmed', schema: {only: ['content']}, csv: true, all: true, csv_options: {generator: {col_sep: ','}}, bulk_ids: comments.map(&:id))) + csv = CSV.parse page.driver.response.body + expect(csv.flatten).to match_array %w[Content something anything] + end + end + end + + context 'with composite primary keys', composite_primary_keys: true do + let!(:fanship) { FactoryBot.create(:fanship) } + + it 'exports to CSV' do + visit export_path(model_name: 'fanship') + click_button 'Export to csv' + is_expected.to have_content fanship.fan.name + is_expected.to have_content fanship.team.name + end end end diff --git a/spec/integration/actions/history_index_spec.rb b/spec/integration/actions/history_index_spec.rb new file mode 100644 index 0000000000..58cbe70430 --- /dev/null +++ b/spec/integration/actions/history_index_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'HistoryIndex action', type: :request, active_record: true do + subject { page } + + let(:user) { FactoryBot.create :user } + let(:paper_trail_test) { FactoryBot.create :paper_trail_test } + before(:each) do + RailsAdmin.config do |config| + config.audit_with :paper_trail, 'User', 'PaperTrail::Version' + end + + PaperTrail::Version.delete_all + with_versioning do + PaperTrail.request.whodunnit = user.id + 30.times do |i| + paper_trail_test.update!(name: "updated name #{i}") + end + end + end + + it 'shows the history' do + visit history_index_path(model_name: 'paper_trail_test') + is_expected.to have_css(%([href="/admin/paper_trail_test/#{paper_trail_test.id}"]), count: 20) + end + + it 'supports pagination' do + visit history_index_path(model_name: 'paper_trail_test', page: 2) + is_expected.to have_css(%([href="/admin/paper_trail_test/#{paper_trail_test.id}"]), count: 11) + end + + it 'supports sorting', js: true do + visit history_index_path(model_name: 'paper_trail_test') + find('th.header', text: 'Item').click + is_expected.to have_css('th.item.headerSortDown') + end + + context "when Kaminari's custom param_name is set" do + before { Kaminari.config.param_name = :pagina } + after { Kaminari.config.param_name = :page } + + it 'picks the page value from params' do + visit history_index_path(model_name: 'paper_trail_test', pagina: 2) + is_expected.to have_css(%([href="/admin/paper_trail_test/#{paper_trail_test.id}"]), count: 11) + end + end +end diff --git a/spec/integration/actions/history_show_spec.rb b/spec/integration/actions/history_show_spec.rb new file mode 100644 index 0000000000..d1b3462550 --- /dev/null +++ b/spec/integration/actions/history_show_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'HistoryShow action', type: :request, active_record: true do + let(:user) { FactoryBot.create :user } + let(:paper_trail_test) { FactoryBot.create :paper_trail_test } + before(:each) do + RailsAdmin.config do |config| + config.audit_with :paper_trail, 'User', 'PaperTrail::Version' + end + + PaperTrail::Version.delete_all + with_versioning do + PaperTrail.request.whodunnit = user.id + 30.times do |i| + paper_trail_test.update!(name: "updated name #{i}") + end + end + end + + it 'shows the history' do + visit history_show_path(model_name: 'paper_trail_test', id: paper_trail_test.id) + expect(all('table#history tbody tr').count).to eq(20) + end + + it 'supports pagination' do + visit history_show_path(model_name: 'paper_trail_test', id: paper_trail_test.id, page: 2) + expect(all('table#history tbody tr').count).to eq(11) + end + + context "when Kaminari's custom param_name is set" do + before { Kaminari.config.param_name = :pagina } + after { Kaminari.config.param_name = :page } + + it 'picks the page value from params' do + visit history_show_path(model_name: 'paper_trail_test', id: paper_trail_test.id, pagina: 2) + expect(all('table#history tbody tr').count).to eq(11) + end + end +end diff --git a/spec/integration/actions/index_spec.rb b/spec/integration/actions/index_spec.rb index 1298950098..6f13ad32aa 100644 --- a/spec/integration/actions/index_spec.rb +++ b/spec/integration/actions/index_spec.rb @@ -1,4 +1,4 @@ -# coding: utf-8 +# frozen_string_literal: true require 'spec_helper' @@ -6,7 +6,7 @@ subject { page } describe 'page' do - it "shows \"List of Models\", should show filters and should show column headers" do + it 'shows "List of Models", should show filters and should show column headers' do RailsAdmin.config.default_items_per_page = 1 2.times { FactoryBot.create :player } # two pages of players visit index_path(model_name: 'player') @@ -22,7 +22,7 @@ # it "has the search box with some prompt text" do is_expected.to have_selector("input[placeholder='Filter']") - # https://github.com/sferik/rails_admin/issues/362 + # https://github.com/railsadminteam/rails_admin/issues/362 # test that no link uses the "wildcard route" with the main # controller and list method # it "does not use the 'wildcard route'" do @@ -59,27 +59,6 @@ @comment = FactoryBot.create(:comment, commentable: @players[2]) end - it 'hides redundant filter options for required fields', js: true do - RailsAdmin.config Player do - list do - field :name do - required true - end - field :team - end - end - - visit index_path(model_name: 'player', f: {name: {'1' => {v: ''}}, team: {'2' => {v: ''}}}) - - within(:select, name: 'f[name][1][o]') do - expect(page.all('option').map(&:value)).to_not include('_present', '_blank') - end - - within(:select, name: 'f[team][2][o]') do - expect(page.all('option').map(&:value)).to include('_present', '_blank') - end - end - it 'allows to query on any attribute' do RailsAdmin.config Player do list do @@ -97,6 +76,13 @@ end end + it 'allows to clear the search query box', js: true do + visit index_path(model_name: 'player', query: @players[0].name) + is_expected.not_to have_content(@players[1].name) + find_button('Reset filters').click + is_expected.to have_content(@players[1].name) + end + it 'allows to filter on one attribute' do RailsAdmin.config Player do list do @@ -148,6 +134,22 @@ is_expected.to have_no_content(@players[3].name) end + it 'allows to filter on has_one relationships' do + @draft = FactoryBot.create(:draft, player: @players[1], college: 'University of Alabama') + RailsAdmin.config Player do + list do + field :name + field :draft do + searchable :college + end + end + end + + visit index_path(model_name: 'player', f: {draft: {'1' => {v: 'Alabama'}}}) + is_expected.to have_content(@players[1].name) + is_expected.to have_css('tbody .name_field', count: 1) + end + it 'allows to disable search on attributes' do RailsAdmin.config Player do list do @@ -303,7 +305,7 @@ it 'displays base filters when no filters are present in the params' do RailsAdmin.config Player do - list { filters([:name, :team]) } + list { filters(%i[name team]) } field :name do default_filter_operator 'is' end @@ -321,7 +323,7 @@ type: 'string', value: '', operator: 'is', - required: true, + operators: %w[_discard like not_like is starts_with ends_with], }, { index: 2, @@ -330,13 +332,31 @@ type: 'belongs_to_association', value: '', operator: nil, - required: false, + operators: %w[_discard like not_like is starts_with ends_with _separator _present _blank], }, ] end + + it 'shows the help text below the search box' do + RailsAdmin.config Player do + list do + search_help 'Use this box to search!' + end + end + visit index_path(model_name: 'player') + is_expected.to have_css('.form-text', text: /Use this box/) + end end - describe "fields" do + describe 'fields' do + before do + if defined?(ActiveRecord) && ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) + RailsAdmin.config Fan do + configure(:fanships) { hide } + configure(:fanship) { hide } + end + end + end it 'shows all by default' do visit index_path(model_name: 'fan') expect(all('th').collect(&:text).delete_if { |t| /^\n*$/ =~ t }). @@ -654,6 +674,41 @@ visit index_path(model_name: 'team') expect(find('tbody tr:nth-child(1) td:nth-child(4)')).to have_content(@players.sort_by(&:id).collect(&:name).join(', ')) end + + describe 'with title attribute' do + it 'does not allow XSS' do + RailsAdmin.config Team do + list do + field :name + end + end + @team = FactoryBot.create :team, name: '" onclick="alert()" "' + visit index_path(model_name: 'team') + expect(find('tbody tr:nth-child(1) td:nth-child(2)')['onclick']).to be_nil + expect(find('tbody tr:nth-child(1) td:nth-child(2)')['title']).to eq '" onclick="alert()" "' + end + + it 'does not break values with HTML tags' do + RailsAdmin.config Player do + list do + field :team + end + end + @player = FactoryBot.create :player, team: FactoryBot.create(:team) + visit index_path(model_name: 'player') + expect(find('tbody tr:nth-child(1) td:nth-child(2)')['title']).to eq @player.team.name + end + end + end + + context 'when no record exists' do + before do + visit index_path(model_name: 'player') + end + + it 'shows "No records found" message' do + is_expected.to have_content('No records found') + end end context 'without pagination' do @@ -662,7 +717,7 @@ visit index_path(model_name: 'player') end - it "shows \"2 results\"" do + it 'shows "2 results"' do is_expected.to have_content('2 players') end end @@ -806,11 +861,10 @@ def visit_page(page) before { @players = players.collect { |h| Player.create(h) } } - it 'is configurable per model' do + it 'has reverse direction by default' do RailsAdmin.config Player do list do sort_by :created_at - sort_reverse true field :name end end @@ -820,31 +874,27 @@ def visit_page(page) end end - it 'has reverse direction by default' do + it 'allows change direction by using field configuration' do RailsAdmin.config Player do list do sort_by :created_at + configure :created_at do + sort_reverse false + end field :name end end visit index_path(model_name: 'player') - player_names_by_date.reverse.each_with_index do |name, i| + player_names_by_date.each_with_index do |name, i| expect(find("tbody tr:nth-child(#{i + 1})")).to have_content(name) end end - it 'allows change default direction' do - RailsAdmin.config Player do - list do - sort_by :created_at - sort_reverse false - field :name - end - end + it 'can be activated by clicking the table header', js: true do visit index_path(model_name: 'player') - player_names_by_date.each_with_index do |name, i| - expect(find("tbody tr:nth-child(#{i + 1})")).to have_content(name) - end + find('th.header', text: 'Name').trigger('click') + is_expected.to have_css('th.name_field.headerSortDown') + expect(all('tbody td.name_field').map(&:text)).to eq @players.map(&:name).sort end end @@ -909,7 +959,7 @@ def visit_page(page) end end end - visit index_path(model_name: 'player', query: player.name[1..-1]) + visit index_path(model_name: 'player', query: player.name[1..]) is_expected.to have_no_content(player.name) end end @@ -950,6 +1000,48 @@ def visit_page(page) end end + describe 'with model scope' do + context 'without default scope' do + let!(:teams) { %w[red yellow blue].map { |color| FactoryBot.create :team, color: color } } + + it 'works', active_record: true do + RailsAdmin.config do |config| + config.model Team do + scope { Team.where(color: %w[red blue]) } + end + end + visit index_path(model_name: 'team') + expect(all(:css, 'td.color_field').map(&:text)).to match_array %w[red blue] + end + + it 'works', mongoid: true do + RailsAdmin.config do |config| + config.model Team do + scope { Team.any_in(color: %w[red blue]) } + end + end + visit index_path(model_name: 'team') + expect(all(:css, 'td.color_field').map(&:text)).to match_array %w[red blue] + end + end + + context 'with default_scope' do + let!(:comments) { %w[something anything].map { |content| FactoryBot.create :comment_confirmed, content: content } } + before do + RailsAdmin.config do |config| + config.model Comment::Confirmed do + scope { Comment::Confirmed.unscoped } + end + end + end + + it 'can be overriden' do + visit index_path(model_name: 'comment~confirmed') + expect(all(:css, 'td.content_field').map(&:text)).to match_array %w[something anything] + end + end + end + describe 'with scopes' do before do RailsAdmin.config do |config| @@ -973,7 +1065,7 @@ def visit_page(page) expect(find('#scope_selector li:nth-child(2)')).to have_content('Red') expect(find('#scope_selector li:nth-child(3)')).to have_content('White') expect(find('#scope_selector li:last')).to have_content('White') - expect(find('#scope_selector li.active')).to have_content('All') + expect(find('#scope_selector li a.active')).to have_content('All') end it 'shows only scoped records' do @@ -984,14 +1076,14 @@ def visit_page(page) is_expected.to have_content(@teams[3].name) visit index_path(model_name: 'team', scope: 'red') - expect(find('#scope_selector li.active')).to have_content('Red') + expect(find('#scope_selector li a.active')).to have_content('Red') is_expected.to have_content(@teams[0].name) is_expected.to have_content(@teams[1].name) is_expected.to have_no_content(@teams[2].name) is_expected.to have_no_content(@teams[3].name) visit index_path(model_name: 'team', scope: 'white') - expect(find('#scope_selector li.active')).to have_content('White') + expect(find('#scope_selector li a.active')).to have_content('White') is_expected.to have_no_content(@teams[0].name) is_expected.to have_no_content(@teams[1].name) is_expected.to have_content(@teams[2].name) @@ -1023,7 +1115,7 @@ def visit_page(page) expect(find('#scope_selector li:nth-child(2)')).to have_content('krasnyj') expect(find('#scope_selector li:nth-child(3)')).to have_content('White') expect(find('#scope_selector li:last')).to have_content('White') - expect(find('#scope_selector li.active')).to have_content('every') + expect(find('#scope_selector li a.active')).to have_content('every') end end @@ -1043,7 +1135,7 @@ def visit_page(page) expect(find('#scope_selector li:nth-child(2)')).to have_content('kr') expect(find('#scope_selector li:nth-child(3)')).to have_content('White') expect(find('#scope_selector li:last')).to have_content('White') - expect(find('#scope_selector li.active')).to have_content('any') + expect(find('#scope_selector li a.active')).to have_content('any') end end end @@ -1113,36 +1205,33 @@ def visit_page(page) end describe 'sidescroll' do - all_team_columns = ['', '', 'Id', 'Created at', 'Updated at', 'Division', 'Name', 'Logo url', 'Team Manager', 'Ballpark', 'Mascot', 'Founded', 'Wins', 'Losses', 'Win percentage', 'Revenue', 'Color', 'Custom field', 'Main Sponsor', 'Players', 'Some Fans', 'Comments'] + all_team_columns = ['', 'Id', 'Created at', 'Updated at', 'Division', 'Name', 'Logo url', 'Team Manager', 'Ballpark', 'Mascot', 'Founded', 'Wins', 'Losses', 'Win percentage', 'Revenue', 'Color', 'Custom field', 'Main Sponsor', 'Players', 'Some Fans', 'Comments', ''] - it "displays all fields on one page when true" do - RailsAdmin.config do |config| - config.sidescroll = true - end + it 'displays all fields on one page' do FactoryBot.create_list :team, 3 visit index_path(model_name: 'team') cols = all('th').collect(&:text) expect(cols[0..4]).to eq(all_team_columns[0..4]) expect(cols).to contain_exactly(*all_team_columns) - expect(page).to have_selector('.ra-sidescroll[data-ra-sidescroll=3]') end - it "displays all fields with custom frozen columns" do - RailsAdmin.config do |config| - config.sidescroll = {num_frozen_columns: 2} + it 'allows fields to be sticky' do + RailsAdmin.config Team do + list do + configure(:division) { sticky true } + configure(:name) { sticky true } + end end FactoryBot.create_list :team, 3 visit index_path(model_name: 'team') cols = all('th').collect(&:text) - expect(cols[0..4]).to eq(all_team_columns[0..4]) + expect(cols[0..4]).to eq(['', 'Division', 'Name', 'Id', 'Created at']) expect(cols).to contain_exactly(*all_team_columns) - expect(page).to have_selector('.ra-sidescroll[data-ra-sidescroll=2]') + expect(page).to have_selector('.name_field.sticky') + expect(page).to have_selector('.division_field.sticky') end - it "displays all fields with no checkboxes" do - RailsAdmin.config do |config| - config.sidescroll = true - end + it 'displays all fields with no checkboxes' do RailsAdmin.config Team do list do checkboxes false @@ -1152,92 +1241,40 @@ def visit_page(page) visit index_path(model_name: 'team') cols = all('th').collect(&:text) expect(cols[0..3]).to eq(all_team_columns[1..4]) - expect(cols).to contain_exactly(*all_team_columns[1..-1]) - expect(page).to have_selector('.ra-sidescroll[data-ra-sidescroll=2]') - end - - it "displays all fields with no frozen columns" do - RailsAdmin.config do |config| - config.sidescroll = {num_frozen_columns: 0} - end - FactoryBot.create_list :team, 3 - visit index_path(model_name: 'team') - cols = all('th').collect(&:text) - expect(cols[0..4]).to eq(all_team_columns[0..4]) - expect(cols).to contain_exactly(*all_team_columns) - expect(page).to have_selector('.ra-sidescroll[data-ra-sidescroll=0]') + expect(cols).to contain_exactly(*all_team_columns[1..]) end + end - it "displays sets when not set" do - visit index_path(model_name: 'team') - expect(all('th').collect(&:text)).to eq ['', 'Id', 'Created at', 'Updated at', 'Division', 'Name', 'Logo url', '...', ''] - expect(page).not_to have_selector('.ra-sidescroll') - end + context 'with composite primary keys', composite_primary_keys: true do + let!(:fanships) { FactoryBot.create_list(:fanship, 3) } - it "displays sets when global config is on but model config is off" do - RailsAdmin.config do |config| - config.sidescroll = true - end - RailsAdmin.config Team do - list do - sidescroll false - end + it 'shows the list' do + visit index_path(model_name: 'fanship') + expect(all('th').collect(&:text)[0..3]).to eq(['', 'Fan', 'Team', 'Since']) + fanships.each do |fanship| + is_expected.to have_content fanship.fan.name + is_expected.to have_content fanship.team.name end - visit index_path(model_name: 'team') - expect(all('th').collect(&:text)).to eq ['', 'Id', 'Created at', 'Updated at', 'Division', 'Name', 'Logo url', '...', ''] - expect(page).not_to have_selector('.ra-sidescroll') + is_expected.to have_content '3 fanships' end - it "displays all fields when global config is off but model config is on" do - RailsAdmin.config Team do - list do - sidescroll true - end - end - FactoryBot.create_list :team, 3 - visit index_path(model_name: 'team') - cols = all('th').collect(&:text) - expect(cols[0..4]).to eq(all_team_columns[0..4]) - expect(cols).to contain_exactly(*all_team_columns) - expect(page).to have_selector('.ra-sidescroll[data-ra-sidescroll=3]') - end + context 'using custom serializer' do + before do + RailsAdmin.config.composite_keys_serializer = Class.new do + def self.serialize(keys) + keys.join(',') + end - it "displays all fields with custom model config settings" do - RailsAdmin.config do |config| - config.sidescroll = true - end - RailsAdmin.config Team do - list do - sidescroll(num_frozen_columns: 2) + def self.deserialize(string) + string.split(',') + end end end - FactoryBot.create_list :team, 3 - FactoryBot.create_list :player, 3 - visit index_path(model_name: 'team') - cols = all('th').collect(&:text) - expect(cols[0..4]).to eq(all_team_columns[0..4]) - expect(cols).to contain_exactly(*all_team_columns) - expect(page).to have_selector('.ra-sidescroll[data-ra-sidescroll=2]') - visit index_path(model_name: 'player') - expect(page).to have_selector('.ra-sidescroll[data-ra-sidescroll=3]') - end - it "displays all fields with model config checkbox settings" do - RailsAdmin.config do |config| - config.sidescroll = true + it 'shows the member action links accordingly' do + visit index_path(model_name: 'fanship') + is_expected.to have_css(%(a[href$="/admin/fanship/#{fanships[0].fan_id},#{fanships[0].team_id}/edit"])) end - RailsAdmin.config Team do - list do - sidescroll(num_frozen_columns: 3) - checkboxes false - end - end - FactoryBot.create_list :team, 3 - visit index_path(model_name: 'team') - cols = all('th').collect(&:text) - expect(cols[0..3]).to eq(all_team_columns[1..4]) - expect(cols).to contain_exactly(*all_team_columns[1..-1]) - expect(page).to have_selector('.ra-sidescroll[data-ra-sidescroll=3]') end end end diff --git a/spec/integration/actions/new_spec.rb b/spec/integration/actions/new_spec.rb index 2e8d2bf725..e8b67965c4 100644 --- a/spec/integration/actions/new_spec.rb +++ b/spec/integration/actions/new_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'New action', type: :request do @@ -8,22 +10,22 @@ visit new_path(model_name: 'player') end - it "shows \"New Model\"" do + it 'shows "New Model"' do is_expected.to have_content('New Player') end - it "shows required fields as \"Required\"" do + it 'shows required fields as "Required"' do is_expected.to have_selector('div', text: /Name\s*Required/) is_expected.to have_selector('div', text: /Number\s*Required/) end - it "shows non-required fields as \"Optional\"" do - is_expected.to have_selector('#player_position_field .help-block', text: 'Optional') - is_expected.to have_selector('#player_born_on_field .help-block', text: 'Optional') - is_expected.to have_selector('#player_notes_field .help-block', text: 'Optional') + it 'shows non-required fields as "Optional"' do + is_expected.to have_selector('#player_position_field .form-text', text: 'Optional') + is_expected.to have_selector('#player_born_on_field .form-text', text: 'Optional') + is_expected.to have_selector('#player_notes_field .form-text', text: 'Optional') end - # https://github.com/sferik/rails_admin/issues/362 + # https://github.com/railsadminteam/rails_admin/issues/362 # test that no link uses the "wildcard route" with the main # controller and new method it "does not use the 'wildcard route'" do @@ -44,15 +46,16 @@ expect(page).to have_css('input[value=Sam]') end - it 'prepropulates belongs to relationships' do - @team = FactoryBot.create :team, name: 'belongs_to association prepopulated' - visit new_path(model_name: 'player', associations: {team: @team.id}) - expect(page).to have_css("select#player_team_id option[selected='selected'][value='#{@team.id}']") + it 'prepropulates has_one relationships' do + @draft = FactoryBot.create :draft + @player = FactoryBot.create :player, name: 'has_one association prepopulated' + visit new_path(model_name: 'player', player: {draft_id: @draft.id}) + expect(page).to have_css("select#player_draft_id option[selected='selected'][value='#{@draft.id}']") end it 'prepropulates has_many relationships' do @player = FactoryBot.create :player, name: 'has_many association prepopulated' - visit new_path(model_name: 'team', associations: {players: @player.id}) + visit new_path(model_name: 'team', team: {player_ids: [@player.id]}) expect(page).to have_css("select#team_player_ids option[selected='selected'][value='#{@player.id}']") end end @@ -120,19 +123,21 @@ end end - context 'on create and add another' do + context 'on create and add another', js: true do before do visit new_path(model_name: 'player') fill_in 'player[name]', with: 'Jackie Robinson' fill_in 'player[number]', with: '42' fill_in 'player[position]', with: 'Second baseman' - click_button 'Save and add another' - - @player = RailsAdmin::AbstractModel.new('Player').first + find_button('Save and add another').trigger('click') end it 'creates an object with correct attributes' do + is_expected.to have_text 'Player successfully created' + expect(page.current_path).to eq('/admin/player/new') + + @player = RailsAdmin::AbstractModel.new('Player').first expect(@player.name).to eq('Jackie Robinson') expect(@player.number).to eq(42) expect(@player.position).to eq('Second baseman') @@ -174,4 +179,32 @@ is_expected.to have_content('Player is cheating') end end + + context 'with a readonly object' do + it 'shows non-editable form' do + RailsAdmin.config do |config| + config.model ReadOnlyComment do + edit do + field :content + end + end + end + visit new_path(model_name: 'read_only_comment') + is_expected.not_to have_css('textarea[name="read_only_comment[content]"]') + is_expected.to have_css('button[name="_save"]:disabled') + end + end + + context 'with composite primary keys', composite_primary_keys: true do + let!(:fan) { FactoryBot.create(:fan) } + let!(:team) { FactoryBot.create(:team) } + + it 'creates an object' do + visit new_path(model_name: 'fanship') + select(fan.name, from: 'Fan') + select(team.name, from: 'Team') + expect { click_button 'Save' }.to change { Fanship.count }.by(1) + expect(Fanship.first.attributes.fetch_values('fan_id', 'team_id')).to eq [fan.id, team.id] + end + end end diff --git a/spec/integration/actions/show_in_app_spec.rb b/spec/integration/actions/show_in_app_spec.rb new file mode 100644 index 0000000000..e79592359a --- /dev/null +++ b/spec/integration/actions/show_in_app_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'ShowInApp action', type: :request do + subject { page } + + describe 'link' do + let!(:player) { FactoryBot.create :player } + + it 'has the data-turbo: false attribute' do + visit index_path(model_name: 'player') + is_expected.to have_selector(%(li[title="Show in app"] a[data-turbo="false"])) + click_link 'Show' + is_expected.to have_selector(%(a[data-turbo="false"]), text: 'Show in app') + end + end +end diff --git a/spec/integration/actions/show_spec.rb b/spec/integration/actions/show_spec.rb index fb9405e958..c08b3619ae 100644 --- a/spec/integration/actions/show_spec.rb +++ b/spec/integration/actions/show_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'Show action', type: :request do @@ -110,7 +112,7 @@ visit show_path(model_name: 'team', id: team.id) - is_expected.to have_selector('dt .name_field.string_type') + is_expected.to have_selector('div .name_field.string_type') end end @@ -118,7 +120,7 @@ it 'is present' do visit show_path(model_name: 'team', id: team.id) - is_expected.to have_selector('dt .name_field.string_type') + is_expected.to have_selector('div .name_field.string_type') end end @@ -142,11 +144,11 @@ is_expected.not_to have_selector('h4', text: 'Basic info') - %w( + %w[ division name logo_url manager ballpark mascot founded wins losses win_percentage revenue - ).each do |field| + ].each do |field| is_expected.not_to have_selector(".#{field}_field") end end @@ -223,15 +225,15 @@ visit show_path(model_name: 'team', id: team.id) - is_expected.to have_selector('.label', text: 'Name') - is_expected.to have_selector('.label', text: 'Logo url') - is_expected.to have_selector('.label', text: 'Division') - is_expected.to have_selector('.label', text: 'Manager (STRING)') - is_expected.to have_selector('.label', text: 'Ballpark (STRING)') + is_expected.to have_selector('.card-header', text: 'Name') + is_expected.to have_selector('.card-header', text: 'Logo url') + is_expected.to have_selector('.card-header', text: 'Division') + is_expected.to have_selector('.card-header', text: 'Manager (STRING)') + is_expected.to have_selector('.card-header', text: 'Ballpark (STRING)') end end - describe "fields" do + describe 'fields' do before do RailsAdmin.config do |c| c.compact_show_view = false @@ -241,11 +243,11 @@ it 'shows all by default' do visit show_path(model_name: 'team', id: team.id) - %w( + %w[ division name logo_url manager ballpark mascot founded wins losses win_percentage revenue players fans - ).each do |field| + ].each do |field| is_expected.to have_selector(".#{field}_field") end end @@ -276,8 +278,8 @@ visit show_path(model_name: 'team', id: team.id) - is_expected.to have_selector('.label', text: 'Team Manager') - is_expected.to have_selector('.label', text: 'Some Fans') + is_expected.to have_selector('.card-header', text: 'Team Manager') + is_expected.to have_selector('.card-header', text: 'Some Fans') end it 'is renameable' do @@ -293,9 +295,9 @@ visit show_path(model_name: 'team', id: team.id) - is_expected.to have_selector('.label', text: 'Renamed field') - is_expected.to have_selector('.label', text: 'Division') - is_expected.to have_selector('.label', text: 'Name') + is_expected.to have_selector('.card-header', text: 'Renamed field') + is_expected.to have_selector('.card-header', text: 'Division') + is_expected.to have_selector('.card-header', text: 'Name') end it 'is renameable by type' do @@ -314,7 +316,7 @@ 'Ballpark (STRING)', 'Mascot (STRING)', 'Founded', 'Wins', 'Losses', 'Win percentage', 'Revenue', 'Players', 'Fans' ].each do |text| - is_expected.to have_selector('.label', text: text) + is_expected.to have_selector('.card-header', text: text) end end @@ -334,7 +336,7 @@ 'Ballpark (STRING)', 'Mascot (STRING)', 'Founded', 'Wins', 'Losses', 'Win percentage', 'Revenue', 'Players', 'Fans' ].each do |text| - is_expected.to have_selector('.label', text: text) + is_expected.to have_selector('.card-header', text: text) end end @@ -366,12 +368,12 @@ visit show_path(model_name: 'team', id: team.id) - %w(Name Logo\ url Manager Ballpark Mascot).each do |text| - is_expected.not_to have_selector('.label', text: text) + ['Name', 'Logo url', 'Manager', 'Ballpark', 'Mascot'].each do |text| + is_expected.not_to have_selector('.card-header', text: text) end - %w(Division Founded Wins Losses Win\ percentage Revenue Players Fans).each do |text| - is_expected.to have_selector('.label', text: text) + ['Division', 'Founded', 'Wins', 'Losses', 'Win percentage', 'Revenue', 'Players', 'Fans'].each do |text| + is_expected.to have_selector('.card-header', text: text) end end @@ -386,12 +388,12 @@ visit show_path(model_name: 'team', id: team.id) - %w(Name Logo\ url Manager Ballpark Mascot).each do |text| - is_expected.not_to have_selector('.label', text: text) + ['Name', 'Logo url', 'Manager', 'Ballpark', 'Mascot'].each do |text| + is_expected.not_to have_selector('.card-header', text: text) end - %w(Division Founded Wins Losses Win\ percentage Revenue Players Fans).each do |text| - is_expected.to have_selector('.label', text: text) + ['Division', 'Founded', 'Wins', 'Losses', 'Win percentage', 'Revenue', 'Players', 'Fans'].each do |text| + is_expected.to have_selector('.card-header', text: text) end end end @@ -415,7 +417,7 @@ visit show_path(model_name: 'team', id: team.id) is_expected.to have_selector('.truncated_name_field') - is_expected.to have_selector('dd', text: 'fo...') + is_expected.to have_selector('.card', text: 'fo...') end end @@ -437,4 +439,32 @@ end end end + + describe 'with model scope' do + let!(:comments) { %w[something anything].map { |content| FactoryBot.create :comment_confirmed, content: content } } + before do + RailsAdmin.config do |config| + config.model Comment::Confirmed do + scope { Comment::Confirmed.unscoped } + end + end + end + + it 'overrides default_scope' do + visit show_path(model_name: 'comment~confirmed', id: comments[0].id) + is_expected.to have_selector('.card-body', text: 'something') + visit show_path(model_name: 'comment~confirmed', id: comments[1].id) + is_expected.to have_selector('.card-body', text: 'anything') + end + end + + context 'with composite primary keys', composite_primary_keys: true do + let(:fanship) { FactoryBot.create(:fanship) } + + it 'shows the object' do + visit show_path(model_name: 'fanship', id: fanship.id) + is_expected.to have_link(fanship.fan.name) + is_expected.to have_link(fanship.team.name) + end + end end diff --git a/spec/integration/history/rails_admin_paper_trail_spec.rb b/spec/integration/auditing/paper_trail_spec.rb similarity index 89% rename from spec/integration/history/rails_admin_paper_trail_spec.rb rename to spec/integration/auditing/paper_trail_spec.rb index da5c95c4e9..7e2d4cb29e 100644 --- a/spec/integration/history/rails_admin_paper_trail_spec.rb +++ b/spec/integration/auditing/paper_trail_spec.rb @@ -1,11 +1,11 @@ +# frozen_string_literal: true + require 'spec_helper' -require 'paper_trail/frameworks/rspec' if defined?(PaperTrail) -RSpec.describe 'RailsAdmin PaperTrail history', active_record: true do +RSpec.describe 'RailsAdmin PaperTrail auditing', active_record: true do before(:each) do - skip 'Requires Ruby >= 2.3' if Rails::VERSION::STRING >= '5.2' && RUBY_VERSION =~ /^2\.2/ RailsAdmin.config do |config| - config.audit_with :paper_trail, 'User', 'PaperTrail::Version' + config.audit_with :paper_trail end end @@ -54,12 +54,12 @@ end it 'creates versions' do - expect(PaperTrail::Version.count).to eq(30) + expect(paper_class_name.constantize.version_class_name.constantize.count).to eq(30) end describe 'model history fetch with auditing adapter' do before(:all) do - @adapter = RailsAdmin::Extensions::PaperTrail::AuditingAdapter.new(nil, 'User', 'PaperTrail::Version') + @adapter = RailsAdmin::Extensions::PaperTrail::AuditingAdapter.new(nil) end it 'fetches on page of history' do @@ -106,7 +106,7 @@ end it '#table returns item class name' do - expect(@version.table.to_s).to eq('PaperTrailTest') + expect(@version.table.to_s).to eq(@model.model.base_class.name) end end @@ -143,7 +143,7 @@ end end - context 'PaperTrailTest' do + context 'with a normal PaperTrail model' do let(:paper_class_name) { 'PaperTrailTest' } let(:paper_factory) { :paper_trail_test } @@ -155,7 +155,7 @@ it_behaves_like :paper_history end - context 'PaperTrailTestSubclass' do + context 'with a subclassed PaperTrail model' do let(:paper_class_name) { 'PaperTrailTestSubclass' } let(:paper_factory) { :paper_trail_test_subclass } @@ -167,7 +167,7 @@ it_behaves_like :paper_history end - context 'PaperTrailTest::SubclassInNamespace' do + context 'with a namespaced PaperTrail model' do let(:paper_class_name) { 'PaperTrailTest::SubclassInNamespace' } let(:paper_factory) { :paper_trail_test_subclass_in_namespace } @@ -178,4 +178,11 @@ it_behaves_like :paper_history end + + context 'with a PaperTrail model with custom version association name' do + let(:paper_class_name) { 'PaperTrailTestWithCustomAssociation' } + let(:paper_factory) { :paper_trail_test_with_custom_association } + + it_behaves_like :paper_history + end end diff --git a/spec/integration/authentication/devise_spec.rb b/spec/integration/authentication/devise_spec.rb new file mode 100644 index 0000000000..3eb2a7725a --- /dev/null +++ b/spec/integration/authentication/devise_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'RailsAdmin Devise Authentication', type: :request do + subject { page } + let!(:user) { FactoryBot.create :user } + + before do + RailsAdmin.config do |config| + config.authenticate_with do + warden.authenticate! scope: :user + end + config.current_user_method(&:current_user) + end + end + + it 'supports logging-in', js: true do + visit dashboard_path + fill_in 'Email', with: user.email + fill_in 'Password', with: 'password' + click_button 'Log in' + is_expected.to have_css 'body.rails_admin' + end + + it 'supports logging-out', js: true do + login_as user + visit dashboard_path + click_link 'Log out' + is_expected.to have_content 'Log in' + end +end diff --git a/spec/integration/authorization/cancancan_spec.rb b/spec/integration/authorization/cancancan_spec.rb index 8691011d4d..c593976ab1 100644 --- a/spec/integration/authorization/cancancan_spec.rb +++ b/spec/integration/authorization/cancancan_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'RailsAdmin CanCanCan Authorization', type: :request do @@ -5,16 +7,15 @@ class Ability include CanCan::Ability def initialize(user) can :access, :rails_admin if user.roles.include? :admin + can :read, :dashboard if user.roles.include? :test_exception - can :read, :dashboard can :access, :rails_admin can :manage, :all can :show_in_app, :all - can [:update, :destroy], Player - cannot [:update, :destroy], Player, retired: true + can %i[update destroy], Player + cannot %i[update destroy], Player, retired: true else - can :read, :dashboard can :manage, Player if user.roles.include? :manage_player can :read, Player, retired: false if user.roles.include? :read_player can :create, Player, suspended: true if user.roles.include? :create_player @@ -64,7 +65,7 @@ def initialize(user) describe 'with read player role' do before do - @user.update(roles: [:admin, :read_player]) + @user.update(roles: %i[admin read_player]) end it 'GET /admin should show Player but not League' do @@ -109,7 +110,7 @@ def initialize(user) describe 'with create and read player role' do before do - @user.update(roles: [:admin, :read_player, :create_player]) + @user.update(roles: %i[admin read_player create_player]) end it 'GET /admin/player/new should render and create record upon submission' do @@ -135,7 +136,7 @@ def initialize(user) it 'POST /admin/player/new with unauthorized attribute value should raise access denied' do visit new_path(model_name: 'player') fill_in 'player[name]', with: 'Jackie Robinson' - uncheck 'player[suspended]' + choose name: 'player[suspended]', option: '0' expect { click_button 'Save' }.to raise_error(CanCan::AccessDenied) end @@ -147,7 +148,7 @@ def initialize(user) describe 'with update and read player role' do before do - @user.update(roles: [:admin, :read_player, :update_player]) + @user.update(roles: %i[admin read_player update_player]) end it 'GET /admin/player/1/edit should render and update record upon submission' do @@ -173,7 +174,7 @@ def initialize(user) it 'PUT /admin/player/new with unauthorized attribute value should raise access denied' do @player = FactoryBot.create :player visit edit_path(model_name: 'player', id: @player.id) - check 'player[retired]' + choose name: 'player[retired]', option: '1' expect { click_button 'Save' }.to raise_error(CanCan::AccessDenied) end @@ -185,7 +186,7 @@ def initialize(user) describe 'with history role' do it 'shows links to history action' do - @user.update(roles: [:admin, :read_player, :history_player]) + @user.update(roles: %i[admin read_player history_player]) @player = FactoryBot.create :player visit index_path(model_name: 'player') @@ -204,7 +205,7 @@ def initialize(user) describe 'with show in app role' do it 'shows links to show in app action' do - @user.update(roles: [:admin, :read_player, :show_in_app_player]) + @user.update(roles: %i[admin read_player show_in_app_player]) @player = FactoryBot.create :player visit index_path(model_name: 'player') @@ -225,7 +226,7 @@ def initialize(user) describe 'with all roles' do it 'shows links to all actions' do - @user.update(roles: [:admin, :manage_player]) + @user.update(roles: %i[admin manage_player]) @player = FactoryBot.create :player visit index_path(model_name: 'player') @@ -246,7 +247,7 @@ def initialize(user) describe 'with destroy and read player role' do before do - @user.update(roles: [:admin, :read_player, :destroy_player]) + @user.update(roles: %i[admin read_player destroy_player]) end it 'GET /admin/player/1/delete should render and destroy record upon submission' do @@ -286,7 +287,7 @@ def initialize(user) describe 'with exception role' do it 'GET /admin/player/bulk_delete should render records which are authorized to' do - @user.update(roles: [:admin, :test_exception]) + @user.update(roles: %i[admin test_exception]) active_player = FactoryBot.create :player, retired: false retired_player = FactoryBot.create :player, retired: true @@ -297,7 +298,7 @@ def initialize(user) end it 'POST /admin/player/bulk_destroy should destroy records which are authorized to' do - @user.update(roles: [:admin, :test_exception]) + @user.update(roles: %i[admin test_exception]) active_player = FactoryBot.create :player, retired: false retired_player = FactoryBot.create :player, retired: true @@ -309,7 +310,11 @@ def initialize(user) describe 'with a custom admin ability' do before do - RailsAdmin.config { |c| c.authorize_with :cancancan, AdminAbility } + RailsAdmin.config do |c| + c.authorize_with :cancancan do + ability_class { AdminAbility } + end + end @user = FactoryBot.create :user login_as @user end diff --git a/spec/integration/authorization/pundit_spec.rb b/spec/integration/authorization/pundit_spec.rb index 8b2d2c297e..9942d1df2c 100644 --- a/spec/integration/authorization/pundit_spec.rb +++ b/spec/integration/authorization/pundit_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'RailsAdmin Pundit Authorization', type: :request do @@ -26,7 +28,7 @@ describe 'with read player role' do before do - @user.update(roles: [:admin, :read_player]) + @user.update(roles: %i[admin read_player]) end it 'GET /admin should show Player but not League' do @@ -48,7 +50,7 @@ describe 'with admin role' do before do - @user.update(roles: [:admin, :manage_player]) + @user.update(roles: %i[admin manage_player]) end it 'GET /admin should show Player but not League' do @@ -78,7 +80,7 @@ describe 'with all roles' do it 'shows links to all actions' do - @user.update(roles: [:admin, :manage_player]) + @user.update(roles: %i[admin manage_player]) @player = FactoryBot.create :player visit index_path(model_name: 'player') @@ -99,26 +101,26 @@ describe 'with create and read player role' do before do - @user.update(roles: [:admin, :read_player, :create_player]) + @user.update(roles: %i[admin read_player create_player]) end it 'POST /admin/player/new with unauthorized attribute value should raise access denied' do visit new_path(model_name: 'player') fill_in 'player[name]', with: 'Jackie Robinson' - uncheck 'player[suspended]' + choose name: 'player[suspended]', option: '0' expect { click_button 'Save' }.to raise_error(Pundit::NotAuthorizedError) end end describe 'with update and read player role' do before do - @user.update(roles: [:admin, :read_player, :update_player]) + @user.update(roles: %i[admin read_player update_player]) end it 'PUT /admin/player/new with unauthorized attribute value should raise access denied' do @player = FactoryBot.create :player visit edit_path(model_name: 'player', id: @player.id) - check 'player[retired]' + choose name: 'player[retired]', option: '1' expect { click_button 'Save' }.to raise_error(Pundit::NotAuthorizedError) end end diff --git a/spec/integration/fields/action_text_spec.rb b/spec/integration/fields/action_text_spec.rb index e58a2680bd..c5ceafad9c 100644 --- a/spec/integration/fields/action_text_spec.rb +++ b/spec/integration/fields/action_text_spec.rb @@ -1,15 +1,44 @@ +# frozen_string_literal: true + require 'spec_helper' -RSpec.describe 'ActionText field', type: :request do - subject { page } +if defined?(ActionText) + RSpec.describe 'ActionText field', type: :request, js: true do + subject { page } + + before do + RailsAdmin.config FieldTest do + edit do + field :action_text_field + end + end + end + + it 'works without error' do + allow(ConsoleLogger).to receive(:warn).with(/ActionText assets should be loaded statically/) + expect { visit new_path(model_name: 'field_test') }.not_to raise_error + is_expected.to have_selector('trix-toolbar') + end + + if RailsAdmin.config.asset_source == :sprockets && Rails.gem_version < Gem::Version.new('7.0') + it 'shows a warning if ActionText assets are loaded dynamically' do + expect(ConsoleLogger).to receive(:warn).with(/ActionText assets should be loaded statically/) + visit new_path(model_name: 'field_test') + is_expected.to have_selector('trix-toolbar') + end - it 'works without error', js: true do - RailsAdmin.config FieldTest do - edit do - field :action_text_field + it 'allows suppressing the warning' do + RailsAdmin.config FieldTest do + edit do + field :action_text_field do + warn_dynamic_load false + end + end + end + expect(ConsoleLogger).not_to receive(:warn).with(/ActionText assets should be loaded statically/) + visit new_path(model_name: 'field_test') + is_expected.to have_selector('trix-toolbar') end end - expect { visit new_path(model_name: 'field_test') }.not_to raise_error - is_expected.to have_selector('trix-toolbar') end -end if defined?(ActionText) +end diff --git a/spec/integration/fields/active_record_enum_spec.rb b/spec/integration/fields/active_record_enum_spec.rb index 47863788d8..8da9e77a87 100644 --- a/spec/integration/fields/active_record_enum_spec.rb +++ b/spec/integration/fields/active_record_enum_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'ActiveRecordEnum field', type: :request, active_record: true do @@ -18,7 +20,7 @@ visit new_path(model_name: 'field_test') is_expected.to have_selector('.enum_type select') is_expected.not_to have_selector('.enum_type select[multiple]') - expect(all('.enum_type option').map(&:text).select(&:present?)).to eq %w(S M L) + expect(all('.enum_type option').map(&:text).select(&:present?)).to eq %w[S M L] end it 'shows current value as selected' do @@ -54,24 +56,24 @@ visit new_path(model_name: 'field_test') is_expected.to have_selector('.enum_type select') is_expected.not_to have_selector('.enum_type select[multiple]') - expect(all('.enum_type option').map(&:text).select(&:present?)).to eq %w(small medium large) + expect(all('.enum_type option').map(&:text).select(&:present?)).to eq %w[small medium large] end it 'shows current value as selected' do visit edit_path(model_name: 'field_test', id: FieldTest.create(integer_enum_field: :large)) - expect(find('.enum_type select').value).to eq "2" + expect(find('.enum_type select').value).to eq '2' end it 'can be updated' do visit edit_path(model_name: 'field_test', id: FieldTest.create(integer_enum_field: :small)) select 'large' click_button 'Save' - expect(FieldTest.first.integer_enum_field).to eq "large" + expect(FieldTest.first.integer_enum_field).to eq 'large' end it 'pre-populates default value' do visit new_path(model_name: 'field_test') - expect(find('.enum_type select').value).to eq "1" + expect(find('.enum_type select').value).to eq '1' end end end diff --git a/spec/integration/fields/active_storage_spec.rb b/spec/integration/fields/active_storage_spec.rb new file mode 100644 index 0000000000..0e8260533f --- /dev/null +++ b/spec/integration/fields/active_storage_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'ActiveStorage field', type: :request, active_record: true do + subject { page } + let(:field_test) { FactoryBot.create :field_test } + before do + # To suppress 'SQLite3::BusyException: database is locked' exception + @original = page.driver.browser.url_blacklist # rubocop:disable Naming/InclusiveLanguage + page.driver.browser.url_blacklist = [%r{/rails/active_storage/representations}] # rubocop:disable Naming/InclusiveLanguage + end + after { page.driver.browser.url_blacklist = @original } # rubocop:disable Naming/InclusiveLanguage + + describe 'direct upload', js: true do + before do + RailsAdmin.config FieldTest do + edit do + field(:active_storage_asset) { direct true } + end + end + end + + it 'works' do + visit edit_path(model_name: 'field_test', id: field_test.id) + attach_file 'Active storage asset', file_path('test.jpg') + expect_any_instance_of(ActiveStorage::DirectUploadsController).to receive(:create).and_call_original + click_button 'Save' + expect(page).to have_content 'Field test successfully updated' + field_test.reload + expect(field_test.active_storage_asset.filename).to eq 'test.jpg' + end + end +end diff --git a/spec/integration/fields/base_spec.rb b/spec/integration/fields/base_spec.rb index 5bfdc2bd53..332720f715 100644 --- a/spec/integration/fields/base_spec.rb +++ b/spec/integration/fields/base_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'Base field', type: :request do @@ -30,7 +32,7 @@ # So we manually cut off first newline character as a workaround here. expect(find_field('field_test[string_field]').value.gsub(/^\n/, '')).to eq('string_field default_value') expect(find_field('field_test[text_field]').value.gsub(/^\n/, '')).to eq('string_field text_field') - expect(find_field('field_test[date_field]').value).to eq(Date.today.to_s) + expect(find('[name="field_test[date_field]"]', visible: false).value).to eq(Date.today.to_s) expect(has_checked_field?('field_test[boolean_field]')).to be_truthy end @@ -39,7 +41,7 @@ field :color, :enum do default_value 'black' enum do - %w(black white) + %w[black white] end end end @@ -51,7 +53,9 @@ RailsAdmin.config(Team) do field :name do render do - bindings[:object].persisted? ? 'Custom Name' : raise(ZeroDivisionError) + raise ZeroDivisionError unless bindings[:object].persisted? + + 'Custom Name' end end end diff --git a/spec/integration/fields/belongs_to_association_spec.rb b/spec/integration/fields/belongs_to_association_spec.rb index e81b199698..78f997f5f0 100644 --- a/spec/integration/fields/belongs_to_association_spec.rb +++ b/spec/integration/fields/belongs_to_association_spec.rb @@ -1,38 +1,46 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'BelongsToAssociation field', type: :request do subject { page } - describe 'with inverse_of option' do - it 'adds a related id to the belongs_to create team link' do - @player = FactoryBot.create :player - visit edit_path(model_name: 'player', id: @player.id) - is_expected.to have_selector("a[data-link='/admin/team/new?associations%5Bplayers%5D=#{@player.id}&modal=true']") - end - - it 'adds a related id to the has_many create team link' do - @team = FactoryBot.create :team - visit edit_path(model_name: 'team', id: @team.id) - is_expected.to have_selector("a[data-link='/admin/player/new?associations%5Bteam%5D=#{@team.id}&modal=true']") - end + it 'does not add a related id to the belongs_to create team link' do + @player = FactoryBot.create :player + visit edit_path(model_name: 'player', id: @player.id) + is_expected.to have_selector("a[data-link='/admin/team/new?modal=true']") end describe 'on create' do - before do - FactoryBot.create :draft - visit new_path(model_name: 'player') - end + let!(:draft) { FactoryBot.create :draft } + let(:team) { FactoryBot.create :team } it 'shows selects' do + visit new_path(model_name: 'player') is_expected.to have_selector('select#player_team_id') end + + context 'with default_value' do + before do + id = team.id + RailsAdmin.config Player do + configure :team do + default_value id + end + end + end + + it 'shows the value as selected' do + visit new_path(model_name: 'player') + expect(find('select#player_team_id').value).to eq team.id.to_s + end + end end describe 'on show' do before do - @player = FactoryBot.create :player @team = FactoryBot.create :team - @player.update(team_id: @team.id) + @player = FactoryBot.create :player, team_id: @team.id visit show_path(model_name: 'player', id: @player.id) end @@ -70,14 +78,74 @@ it 'allows update', js: true do visit edit_path(model_name: 'managed_team', id: teams[0].id) find('input.ra-filtering-select-input').set('M') - page.execute_script("$('input.ra-filtering-select-input').trigger('focus')") - page.execute_script("$('input.ra-filtering-select-input').trigger('keydown')") + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') - page.execute_script %{$('ul.ui-autocomplete li.ui-menu-item a:contains("ManagingUser ##{users[1].id}")').trigger('mouseenter').click()} + page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("ManagingUser ##{users[1].id}")).click()} click_button 'Save' teams[0].reload expect(teams[0].user).to eq users[1] end end end + + context 'with composite foreign keys', composite_primary_keys: true do + let!(:fanship) { FactoryBot.create(:fanship) } + let(:favorite_player) { FactoryBot.create(:favorite_player) } + + describe 'via default field' do + it 'allows update' do + visit edit_path(model_name: 'favorite_player', id: favorite_player.id) + is_expected.to have_select('Fanship', selected: "Fanship ##{favorite_player.fanship.id}") + select("Fanship ##{fanship.id}", from: 'Fanship') + click_button 'Save' + is_expected.to have_content 'Favorite player successfully updated' + expect(FavoritePlayer.all.map(&:fanship)).to eq [fanship] + end + + context 'with invalid key' do + before do + allow_any_instance_of(RailsAdmin::Config::Fields::Types::BelongsToAssociation). + to receive(:collection).and_return([["Fanship ##{fanship.id}", 'invalid']]) + end + + it 'fails to update' do + visit edit_path(model_name: 'favorite_player', id: favorite_player.id) + select("Fanship ##{fanship.id}", from: 'Fanship') + click_button 'Save' + is_expected.to have_content 'Fanship must exist' + end + end + end + + describe 'via remote-sourced field' do + before do + RailsAdmin.config FavoritePlayer do + field :fanship do + associated_collection_cache_all false + end + end + end + + it 'allows update', js: true do + visit edit_path(model_name: 'favorite_player', id: favorite_player.id) + find('.fanship_field input.ra-filtering-select-input').set(fanship.fan_id) + page.execute_script("document.querySelector('.fanship_field input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") + expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') + page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Fanship ##{fanship.id}")).click()} + click_button 'Save' + is_expected.to have_content 'Favorite player successfully updated' + expect(FavoritePlayer.all.map(&:fanship)).to eq [fanship] + end + end + + describe 'via nested field' do + it 'allows update' do + visit edit_path(model_name: 'nested_favorite_player', id: favorite_player.id) + fill_in 'Since', with: '2020-01-23' + click_button 'Save' + is_expected.to have_content 'Nested favorite player successfully updated' + expect(favorite_player.reload.fanship.since).to eq Date.new(2020, 1, 23) + end + end + end end diff --git a/spec/integration/fields/boolean_spec.rb b/spec/integration/fields/boolean_spec.rb new file mode 100644 index 0000000000..3370a78466 --- /dev/null +++ b/spec/integration/fields/boolean_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Boolean field', type: :request do + subject { page } + let(:field_test) { FactoryBot.create :field_test } + + context 'if nullable' do + before do + RailsAdmin.config FieldTest do + field :boolean_field + end + end + + it 'shows 3 radio buttons' do + visit new_path(model_name: 'field_test') + is_expected.to have_content 'New Field test' + expect(all('[name="field_test[boolean_field]"]').map { |e| e['value'] }).to eq ['1', '0', ''] + end + + it 'can be updated' do + visit edit_path(model_name: 'field_test', id: field_test.id) + + # change the value to true and assert the values + find('.boolean_type label.success').click + click_button 'Save and edit' + # validate that the success button rendered and is active + expect(page).to have_selector('.boolean_type input[value="1"][checked]') + # validate the value is true + expect(field_test.reload.boolean_field).to be true + + # change the value to false and assert the values + find('.boolean_type label.danger').click + click_button 'Save and edit' + expect(page).to have_selector('.boolean_type input[value="0"][checked]') + expect(field_test.reload.boolean_field).to be false + + # change the value to nil and assert the values + find('.boolean_type label.default').click + click_button 'Save and edit' + expect(page).to have_selector('.boolean_type input[value=""][checked]') + expect(field_test.reload.boolean_field).to be nil + end + end + + context 'when the boolean is in an embedded document' do + before do + RailsAdmin.config FieldTest do + field :comment + end + + RailsAdmin.config Comment do + field :content, :boolean + end + end + + it 'can be updated', js: true do + visit edit_path(model_name: 'field_test', id: field_test.id) + + # toggle open the embedded document section + find('#field_test_comment_attributes_field .add_nested_fields').click + # set the value to false and assert the values + find('.boolean_type label.danger').click + click_button 'Save and edit' + expect(field_test.reload.comment.content).to eq '0' + end + end + + context 'if not nullable' do + before do + RailsAdmin.config FieldTest do + field :boolean_field do + nullable false + end + end + end + + it 'shows a checkbox' do + visit new_path(model_name: 'field_test') + is_expected.to have_content 'New Field test' + is_expected.to have_css '[type="checkbox"][name="field_test[boolean_field]"]' + end + + it 'can be updated' do + visit edit_path(model_name: 'field_test', id: field_test.id) + find('.boolean_type input').check + click_button 'Save and edit' + expect(field_test.reload.boolean_field).to be true + find('.boolean_type input').uncheck + click_button 'Save and edit' + expect(field_test.reload.boolean_field).to be false + end + end + + context 'if the database column is not nullable', active_record: true do + before do + RailsAdmin.config FieldTest do + field :non_nullable_boolean_field + end + end + + it 'shows a checkbox' do + visit new_path(model_name: 'field_test') + is_expected.to have_content 'New Field test' + is_expected.to have_css '[type="checkbox"][name="field_test[non_nullable_boolean_field]"]' + end + + it 'can be updated' do + visit edit_path(model_name: 'field_test', id: field_test.id) + find('.boolean_type input').check + click_button 'Save and edit' + expect(field_test.reload.non_nullable_boolean_field).to be true + find('.boolean_type input').uncheck + click_button 'Save and edit' + expect(field_test.reload.non_nullable_boolean_field).to be false + end + end +end diff --git a/spec/integration/fields/carrierwave_spec.rb b/spec/integration/fields/carrierwave_spec.rb index 8ffca19417..abe264bf96 100644 --- a/spec/integration/fields/carrierwave_spec.rb +++ b/spec/integration/fields/carrierwave_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'Carrierwave field', type: :request, active_record: true do @@ -13,7 +15,7 @@ it 'supports caching an uploaded file' do visit new_path(model_name: 'field_test') - attach_file "Carrierwave asset", file_path('test.jpg') + attach_file 'Carrierwave asset', file_path('test.jpg') fill_in 'field_test[string_field]', with: 'Invalid' click_button 'Save' expect(page).to have_content 'Field test failed to be created' diff --git a/spec/integration/fields/ck_editor_spec.rb b/spec/integration/fields/ck_editor_spec.rb index 41a132a412..b065318753 100644 --- a/spec/integration/fields/ck_editor_spec.rb +++ b/spec/integration/fields/ck_editor_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'CKEditor field', type: :request do diff --git a/spec/integration/fields/code_mirror_spec.rb b/spec/integration/fields/code_mirror_spec.rb index dcf33e838d..4c7e47a4c2 100644 --- a/spec/integration/fields/code_mirror_spec.rb +++ b/spec/integration/fields/code_mirror_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'CodeMirror field', type: :request do diff --git a/spec/integration/fields/color_spec.rb b/spec/integration/fields/color_spec.rb index fe48318340..db25bda733 100644 --- a/spec/integration/fields/color_spec.rb +++ b/spec/integration/fields/color_spec.rb @@ -1,15 +1,15 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'Color field', type: :request do subject { page } - it 'shows input with class color' do + it 'uses HTML5 color picker' do RailsAdmin.config Team do - edit do - field :color, :color - end + field :color, :color end visit new_path(model_name: 'team') - is_expected.to have_selector('.color_type input') + is_expected.to have_selector('#team_color[type="color"]') end end diff --git a/spec/integration/fields/date_spec.rb b/spec/integration/fields/date_spec.rb new file mode 100644 index 0000000000..9e8c0239db --- /dev/null +++ b/spec/integration/fields/date_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Date field', type: :request do + subject { page } + before do + RailsAdmin.config FieldTest do + field :id + field :date_field + end + end + + describe 'filtering' do + let!(:field_tests) do + [FactoryBot.create(:field_test, date_field: Date.new(2021, 1, 2)), + FactoryBot.create(:field_test, date_field: Date.new(2021, 1, 3))] + end + + it 'correctly returns a record' do + visit index_path(model_name: 'field_test', f: {date_field: {'1' => {v: [nil, '2021-01-03T00:00:00', nil], o: 'between'}}}) + is_expected.to have_content '1 field test' + is_expected.to have_content field_tests[1].id + end + + it 'does not break when the condition is not filled' do + visit index_path(model_name: 'field_test', f: {date_field: {'1' => {v: [nil, '', ''], o: 'between'}}}) + is_expected.to have_content '2 field test' + end + + context 'with server timezone changed' do + around do |example| + original = Time.zone + Time.zone = ActiveSupport::TimeZone.new('Central Time (US & Canada)') + example.run + Time.zone = original + end + + it 'correctly returns a record' do + visit index_path(model_name: 'field_test', f: {date_field: {'1' => {v: [nil, '2021-01-03T00:00:00', nil], o: 'between'}}}) + is_expected.to have_content '1 field test' + is_expected.to have_content field_tests[1].id + end + end + end + + context 'on create' do + it 'is initially blank' do + visit new_path(model_name: 'field_test') + expect(find('[name="field_test[date_field]"]', visible: false).value).to be_blank + end + + it 'persists the value' do + visit new_path(model_name: 'field_test') + find('[name="field_test[date_field]"]', visible: false).set('2021-01-02T00:00:00') + click_button 'Save' + expect(FieldTest.count).to eq 1 + expect(FieldTest.first.date_field).to eq Date.new(2021, 1, 2) + end + + it 'ignores the time part' do + visit new_path(model_name: 'field_test') + find('[name="field_test[date_field]"]', visible: false).set('2021-01-02T12:34:00') + click_button 'Save' + expect(FieldTest.count).to eq 1 + expect(FieldTest.first.date_field).to eq Date.new(2021, 1, 2) + end + end + + context 'on update' do + let(:field_test) { FactoryBot.create :field_test, date_field: Date.new(2021, 1, 2) } + + it 'updates the value' do + visit edit_path(model_name: 'field_test', id: field_test.id) + expect(find('[name="field_test[date_field]"]', visible: false).value).to eq '2021-01-02T00:00:00' + find('[name="field_test[date_field]"]', visible: false).set('2021-02-03T00:00:00') + click_button 'Save' + field_test.reload + expect(field_test.date_field).to eq Date.new(2021, 2, 3) + end + end + + context 'with server timezone changed' do + let(:field_test) { FactoryBot.create :field_test, date_field: Date.new(2015, 10, 8) } + + around do |example| + original = Time.zone + Time.zone = ActiveSupport::TimeZone.new('Central Time (US & Canada)') + example.run + Time.zone = original + end + + it 'is not altered by just saving untouched' do + visit edit_path(model_name: 'field_test', id: field_test.id) + expect(find('[name="field_test[date_field]"]', visible: false).value).to eq '2015-10-08T00:00:00' + click_button 'Save' + expect { field_test.reload }.not_to change(field_test, :date_field) + end + end +end diff --git a/spec/integration/fields/datetime_spec.rb b/spec/integration/fields/datetime_spec.rb new file mode 100644 index 0000000000..96a334934a --- /dev/null +++ b/spec/integration/fields/datetime_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Datetime field', type: :request do + subject { page } + before do + RailsAdmin.config FieldTest do + edit do + field :datetime_field + end + end + end + + describe 'filtering' do + let!(:field_tests) do + [FactoryBot.create(:field_test, datetime_field: DateTime.new(2021, 1, 2, 3, 45)), + FactoryBot.create(:field_test, datetime_field: DateTime.new(2021, 1, 2, 4, 45))] + end + + it 'correctly returns a record' do + visit index_path(model_name: 'field_test', f: {datetime_field: {'1' => {v: [nil, '2021-01-02T04:00:00', nil], o: 'between'}}}) + is_expected.to have_content '1 field test' + is_expected.to have_content field_tests[1].id + end + + it 'does not break when the condition is not filled' do + visit index_path(model_name: 'field_test', f: {datetime_field: {'1' => {v: [nil, '', ''], o: 'between'}}}) + is_expected.to have_content '2 field test' + end + + context 'with server timezone changed' do + around do |example| + original = Time.zone + Time.zone = ActiveSupport::TimeZone.new('Central Time (US & Canada)') + example.run + Time.zone = original + end + + it 'correctly returns a record' do + visit index_path(model_name: 'field_test', f: {datetime_field: {'1' => {v: [nil, '2021-01-01T22:00:00', nil], o: 'between'}}}) + is_expected.to have_content '1 field test' + is_expected.to have_content field_tests[1].id + end + end + end + + context 'on create' do + it 'is initially blank' do + visit new_path(model_name: 'field_test') + expect(find('[name="field_test[datetime_field]"]', visible: false).value).to be_blank + end + + it 'persists the value' do + visit new_path(model_name: 'field_test') + find('[name="field_test[datetime_field]"]', visible: false).set('2021-01-02T03:45:00') + click_button 'Save' + expect(FieldTest.count).to eq 1 + expect(FieldTest.first.datetime_field).to eq DateTime.new(2021, 1, 2, 3, 45) + end + end + + context 'on update' do + let(:field_test) { FactoryBot.create :field_test, datetime_field: DateTime.new(2021, 1, 2, 3, 45) } + + it 'updates the value' do + visit edit_path(model_name: 'field_test', id: field_test.id) + expect(find('[name="field_test[datetime_field]"]', visible: false).value).to eq '2021-01-02T03:45:00' + find('[name="field_test[datetime_field]"]', visible: false).set('2021-02-03T04:55:00') + click_button 'Save' + field_test.reload + expect(field_test.datetime_field).to eq DateTime.new(2021, 2, 3, 4, 55) + end + end + + context 'with server timezone changed' do + let(:field_test) { FactoryBot.create :field_test, datetime_field: DateTime.new(2015, 10, 8, 6, 45) } + + around do |example| + original = Time.zone + Time.zone = ActiveSupport::TimeZone.new('Central Time (US & Canada)') + example.run + Time.zone = original + end + + it 'treats the datetime set by the browser as local time' do + visit edit_path(model_name: 'field_test', id: field_test.id) + find('[name="field_test[datetime_field]"]', visible: false).set('2021-02-03T04:55:00') + click_button 'Save' + field_test.reload + expect(field_test.datetime_field.iso8601).to eq '2021-02-03T04:55:00-06:00' + end + + it 'is not altered by just saving untouched' do + visit edit_path(model_name: 'field_test', id: field_test.id) + expect(find('[name="field_test[datetime_field]"]', visible: false).value).to eq '2015-10-08T01:45:00' + click_button 'Save' + expect { field_test.reload }.not_to change(field_test, :datetime_field) + end + end +end diff --git a/spec/integration/fields/enum_spec.rb b/spec/integration/fields/enum_spec.rb index 1aed224a9b..d0c3e66248 100644 --- a/spec/integration/fields/enum_spec.rb +++ b/spec/integration/fields/enum_spec.rb @@ -1,184 +1,53 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'Enum field', type: :request, active_record: true do subject { page } - describe "when object responds to '\#{method}_enum'" do - before do - Team.class_eval do - def color_enum - %w(blue green red) - end - end - RailsAdmin.config Team do - edit do - field :color - end - end - visit new_path(model_name: 'team') - end - - after do - Team.send(:remove_method, :color_enum) - end - - it 'auto-detects enumeration' do - is_expected.to have_selector('.enum_type select') - is_expected.not_to have_selector('.enum_type select[multiple]') - expect(all('.enum_type option').map(&:text).select(&:present?)).to eq %w(blue green red) - end + before do + allow_any_instance_of(Team).to receive(:color_enum).and_return(%w[blue green red]) end - describe "when class responds to '\#{method}_enum'" do + describe 'for single value' do before do - Team.instance_eval do - def color_enum - %w(blue green red) - end - end RailsAdmin.config Team do - edit do - field :color - end + field :color end - visit new_path(model_name: 'team') end - after do - Team.instance_eval { undef :color_enum } - end - - it 'auto-detects enumeration' do - is_expected.to have_selector('.enum_type select') - is_expected.to have_content('green') - end - end - - describe 'the enum instance method' do - before do - Team.class_eval do - def color_list - %w(blue green red) - end - end - RailsAdmin.config Team do - edit do - field :color, :enum do - enum_method :color_list - end - end - end + it 'shows a single-value edit form' do visit new_path(model_name: 'team') - end - - after do - Team.send(:remove_method, :color_list) - end - - it 'allows configuration' do is_expected.to have_selector('.enum_type select') - is_expected.to have_content('green') + is_expected.not_to have_selector('.enum_type select[multiple]') + expect(all('.enum_type option').map(&:text).select(&:present?)).to eq %w[blue green red] end - end - describe 'the enum class method' do - before do - Team.instance_eval do - def color_list - %w(blue green red) - end - end - RailsAdmin.config Team do - edit do - field :color, :enum do - enum_method :color_list - end - end - end + it 'uses the filtering-select widget for selection', js: true do visit new_path(model_name: 'team') - end - - after do - Team.instance_eval { undef :color_list } - end - - it 'allows configuration' do - is_expected.to have_selector('.enum_type select') - is_expected.to have_content('green') + is_expected.to have_selector('.enum_type .filtering-select') end end - describe 'when overriding enum configuration' do + describe 'for multiple values' do before do - Team.class_eval do - def color_list - %w(blue green red) - end - end RailsAdmin.config Team do - edit do - field :color, :enum do - enum_method :color_list - enum do - %w(yellow black) - end - end + field :color do + multiple true end end - visit new_path(model_name: 'team') end - after do - Team.send(:remove_method, :color_list) - end - - it 'allows direct listing of enumeration options and override enum method' do + it 'shows a multiple-value edit form' do + visit new_path(model_name: 'team') is_expected.to have_selector('.enum_type select') - is_expected.to have_no_content('green') - is_expected.to have_content('yellow') - end - end - - describe 'when serialize is enabled in ActiveRecord model', active_record: true do - before do - class TeamWithSerializedEnum < Team - self.table_name = 'teams' - serialize :color - def color_enum - %w(blue green red) - end - end - RailsAdmin.config do |c| - c.included_models = [TeamWithSerializedEnum] - end - visit new_path(model_name: 'team_with_serialized_enum') - end - - it 'makes enumeration multi-selectable' do is_expected.to have_selector('.enum_type select[multiple]') + expect(all('.enum_type option').map(&:text).select(&:present?)).to eq %w[blue green red] end - end - describe 'when serialize is enabled in Mongoid model', mongoid: true do - before do - Team.instance_eval do - field :color, type: Array - def color_enum - %w(blue green red) - end - end + it 'uses the filtering-multiselect widget for selection', js: true do visit new_path(model_name: 'team') - end - - after do - Team.instance_eval do - field :color, type: String - undef :color_enum - end - end - - it 'makes enumeration multi-selectable' do - is_expected.to have_selector('.enum_type select[multiple]') + is_expected.to have_selector('.enum_type .ra-multiselect') end end end diff --git a/spec/integration/fields/file_upload_spec.rb b/spec/integration/fields/file_upload_spec.rb new file mode 100644 index 0000000000..ae5002f269 --- /dev/null +++ b/spec/integration/fields/file_upload_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'FileUpload field', type: :request do + subject { page } + + before do + RailsAdmin.config FieldTest do + field :string_field, :file_upload do + delete_method 'boolean_field' + def resource_url(_thumb = false) + value + end + end + end + end + let(:field_test) { FactoryBot.create :field_test, string_field: 'http://localhost/dummy.jpg' } + + it 'supports deletion', js: true do + visit edit_path(model_name: 'field_test', id: field_test.id) + expect(find('#field_test_boolean_field', visible: false)).not_to be_checked + click_link "Delete 'String field'" + expect(find('#field_test_boolean_field', visible: false)).to be_checked + end + + it 'shows a inline preview', js: true do + visit new_path(model_name: 'field_test') + attach_file 'String field', file_path('test.jpg') + is_expected.to have_selector('#field_test_string_field_field img.preview') + end +end diff --git a/spec/integration/fields/floara_spec.rb b/spec/integration/fields/floara_spec.rb index c05aec9b84..bf55431cfd 100644 --- a/spec/integration/fields/floara_spec.rb +++ b/spec/integration/fields/floara_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'Floara field', type: :request do @@ -25,6 +27,6 @@ end visit new_path(model_name: 'draft') - is_expected.to have_selector("textarea#draft_notes[data-richtext=\"froala-wysiwyg\"][data-options]") + is_expected.to have_selector('textarea#draft_notes[data-richtext="froala-wysiwyg"][data-options]') end end diff --git a/spec/integration/fields/has_and_belongs_to_many_association_spec.rb b/spec/integration/fields/has_and_belongs_to_many_association_spec.rb index fd60afd949..9649d0b83d 100644 --- a/spec/integration/fields/has_and_belongs_to_many_association_spec.rb +++ b/spec/integration/fields/has_and_belongs_to_many_association_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'HasAndBelongsToManyAssociation field', type: :request do @@ -52,4 +54,27 @@ is_expected.not_to have_css("a[href='/admin/comment/#{@comment3.id}']") end end + + context "with Mongoid's custom primary_key option", mongoid: true do + let(:user) { FactoryBot.create :managing_user, players: [players[0]], balls: [balls[0]] } + let!(:players) { FactoryBot.create_list(:player, 2) } + let!(:balls) { %w[red blue].map { |color| FactoryBot.create(:ball, color: color) } } + before do + RailsAdmin.config ManagingUser do + field :players + field :balls + end + end + + it 'allows update' do + visit edit_path(model_name: 'managing_user', id: user.id) + expect(find("select#managing_user_player_names option[value=\"#{players[0].name}\"]")).to be_selected + expect(find("select#managing_user_ball_ids option[value=\"#{balls[0].color}\"]")).to be_selected + select(players[1].name, from: 'Players') + select(balls[1].rails_admin_default_object_label_method, from: 'Balls') + click_button 'Save' + expect(ManagingUser.first.players).to match_array players + expect(ManagingUser.first.balls).to match_array balls + end + end end diff --git a/spec/integration/fields/has_many_association_spec.rb b/spec/integration/fields/has_many_association_spec.rb index 525ee385a9..e2e2fde6e0 100644 --- a/spec/integration/fields/has_many_association_spec.rb +++ b/spec/integration/fields/has_many_association_spec.rb @@ -1,8 +1,16 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'HasManyAssociation field', type: :request do subject { page } + it 'adds a related id to the has_many create team link' do + @team = FactoryBot.create :team + visit edit_path(model_name: 'team', id: @team.id) + is_expected.to have_selector("a[data-link='/admin/player/new?modal=true&player%5Bteam_id%5D=#{@team.id}']") + end + context 'when an association is readonly' do it 'is not editable' do @league = FactoryBot.create :league @@ -39,20 +47,31 @@ expect(@league.divisions).not_to include(@divisions[1]) expect(@league.divisions).not_to include(@divisions[2]) end + + context 'with default_value' do + before do + ids = [@divisions[2].id] + RailsAdmin.config League do + configure :divisions do + default_value ids + end + end + end + + it 'shows the value as selected' do + visit new_path(model_name: 'league') + expect(find('select#league_division_ids').value).to eq [@divisions[2].id.to_s] + end + end end context 'on update' do it 'is fillable and emptyable', active_record: true do - RailsAdmin.config do |c| - c.audit_with :history - end - @league = FactoryBot.create :league @divisions = Array.new(3) { Division.create!(name: "div #{Time.now.to_f}", league: League.create!(name: "league #{Time.now.to_f}")) } put edit_path(model_name: 'league', id: @league.id, league: {name: 'National League', division_ids: [@divisions[0].id]}) - old_name = @league.name @league.reload expect(@league.name).to eq('National League') @divisions[0].reload @@ -60,8 +79,6 @@ expect(@league.divisions).not_to include(@divisions[1]) expect(@league.divisions).not_to include(@divisions[2]) - expect(RailsAdmin::History.where(item: @league.id).collect(&:message)).to include("name: \"#{old_name}\" -> \"National League\"") - put edit_path(model_name: 'league', id: @league.id, league: {division_ids: ['']}) @league.reload @@ -112,123 +129,6 @@ end end - describe 'nested form' do - it 'works', js: true do - @record = FactoryBot.create :field_test - NestedFieldTest.create! title: 'title 1', field_test: @record - NestedFieldTest.create! title: 'title 2', field_test: @record - visit edit_path(model_name: 'field_test', id: @record.id) - - fill_in 'field_test_nested_field_tests_attributes_0_title', with: 'nested field test title 1 edited', visible: false - find('#field_test_nested_field_tests_attributes_1__destroy', visible: false).set('true') - - # trigger click via JS, workaround for instability in CI - execute_script %($('button[name="_save"]').trigger('click');) - is_expected.to have_content('Field test successfully updated') - - @record.reload - expect(@record.nested_field_tests.length).to eq(1) - expect(@record.nested_field_tests[0].title).to eq('nested field test title 1 edited') - end - - it 'supports adding new nested item', js: true do - @record = FactoryBot.create :field_test - visit edit_path(model_name: 'field_test', id: @record.id) - - find('#field_test_nested_field_tests_attributes_field .add_nested_fields').click - - expect(page).to have_selector('.fields.tab-pane.active', visible: true) - end - - it 'sets bindings[:object] to nested object' do - RailsAdmin.config(NestedFieldTest) do - nested do - field :title do - label do - bindings[:object].class.name - end - end - end - end - @record = FieldTest.create - NestedFieldTest.create! title: 'title 1', field_test: @record - visit edit_path(model_name: 'field_test', id: @record.id) - expect(find('#field_test_nested_field_tests_attributes_0_title_field')).to have_content('NestedFieldTest') - end - - it 'is deactivatable' do - visit new_path(model_name: 'field_test') - is_expected.to have_selector('#field_test_nested_field_tests_attributes_field .add_nested_fields') - RailsAdmin.config(FieldTest) do - configure :nested_field_tests do - nested_form false - end - end - visit new_path(model_name: 'field_test') - is_expected.to have_no_selector('#field_test_nested_field_tests_attributes_field .add_nested_fields') - end - - context 'with nested_attributes_options given' do - before do - allow(FieldTest.nested_attributes_options).to receive(:[]).with(any_args). - and_return(allow_destroy: true, update_only: false) - end - - it 'does not show add button when :update_only is true' do - allow(FieldTest.nested_attributes_options).to receive(:[]).with(:nested_field_tests). - and_return(allow_destroy: true, update_only: true) - visit new_path(model_name: 'field_test') - is_expected.to have_selector('.toggler') - is_expected.not_to have_selector('#field_test_nested_field_tests_attributes_field .add_nested_fields') - end - - it 'does not show destroy button except for newly created when :allow_destroy is false' do - @record = FieldTest.create - NestedFieldTest.create! title: 'nested title 1', field_test: @record - allow(FieldTest.nested_attributes_options).to receive(:[]).with(:nested_field_tests). - and_return(allow_destroy: false, update_only: false) - visit edit_path(model_name: 'field_test', id: @record.id) - expect(find('#field_test_nested_field_tests_attributes_0_title').value).to eq('nested title 1') - is_expected.not_to have_selector('form .remove_nested_fields') - expect(find('div#nested_field_tests_fields_blueprint', visible: false)[:'data-blueprint']).to match( - /]* class="remove_nested_fields"[^>]*>/, - ) - end - end - - context "when a field which have the same name of nested_in field's" do - it "does not hide fields which are not associated with nesting parent field's model" do - visit new_path(model_name: 'field_test') - is_expected.not_to have_selector('select#field_test_nested_field_tests_attributes_new_nested_field_tests_field_test_id') - expect(find('div#nested_field_tests_fields_blueprint', visible: false)[:'data-blueprint']).to match( - /]* id="field_test_nested_field_tests_attributes_new_nested_field_tests_another_field_test_id"[^>]*>/, - ) - end - - it 'hides fields that are deeply nested with inverse_of' do - visit new_path(model_name: 'field_test') - expect(page.body).to_not include('field_test_nested_field_tests_attributes_new_nested_field_tests_deeply_nested_field_tests_attributes_new_deeply_nested_field_tests_nested_field_test_id_field') - expect(page.body).to include('field_test_nested_field_tests_attributes_new_nested_field_tests_deeply_nested_field_tests_attributes_new_deeply_nested_field_tests_title') - end - end - - context 'when XSS attack is attempted', js: true do - it 'does not break on adding a new item' do - allow(I18n).to receive(:t).and_call_original - expect(I18n).to receive(:t).with('admin.form.new_model', name: 'Nested field test').and_return('') - @record = FactoryBot.create :field_test - visit edit_path(model_name: 'field_test', id: @record.id) - find('#field_test_nested_field_tests_attributes_field .add_nested_fields').click - end - - it 'does not break on editing an existing item' do - @record = FactoryBot.create :field_test - NestedFieldTest.create! title: '', field_test: @record - visit edit_path(model_name: 'field_test', id: @record.id) - end - end - end - context 'with not nullable foreign key', active_record: true do before do RailsAdmin.config FieldTest do @@ -275,11 +175,12 @@ end end - it "allows update" do + it 'allows update' do visit edit_path(model_name: 'managing_user', id: user.id) expect(find("select#managing_user_team_ids option[value=\"#{teams[0].id}\"]")).to have_content teams[0].name select(teams[1].name, from: 'Teams') click_button 'Save' + is_expected.to have_content 'Managing user successfully updated' expect(ManagingUser.first.teams).to match_array teams end @@ -290,16 +191,99 @@ end end - it "allows update", js: true do + it 'allows update', js: true do visit edit_path(model_name: 'managing_user', id: user.id) find('input.ra-multiselect-search').set('T') - page.execute_script("$('input.ra-multiselect-search').trigger('focus')") - page.execute_script("$('input.ra-multiselect-search').trigger('keydown')") find('.ra-multiselect-collection option', text: teams[1].name).select_option find('.ra-multiselect-item-add').click click_button 'Save' + is_expected.to have_content 'Managing user successfully updated' expect(ManagingUser.first.teams).to match_array teams end end end + + context 'with composite foreign keys', composite_primary_keys: true do + let(:fan) { FactoryBot.create(:fan) } + let!(:fanships) { FactoryBot.create_list(:fanship, 3) } + + describe 'via default field' do + before do + RailsAdmin.config Fan do + field :name + field :fanships + end + end + + it 'shows the current selection' do + visit edit_path(model_name: 'fan', id: fanships[0].fan.id) + is_expected.to have_select('Fanships', selected: "Fanship ##{fanships[0].id}") + end + + it 'allows update' do + visit edit_path(model_name: 'fan', id: fan.id) + select("Fanship ##{fanships[0].id}", from: 'Fanships') + select("Fanship ##{fanships[1].id}", from: 'Fanships') + click_button 'Save' + is_expected.to have_content 'Fan successfully updated' + expect(fan.reload.fanships.map(&:team_id)).to match_array fanships.map(&:team_id)[0..1] + end + + context 'with invalid key' do + before do + allow_any_instance_of(RailsAdmin::Config::Fields::Types::HasManyAssociation). + to receive(:collection).and_return([["Fanship ##{fanships[0].id}", 'invalid']]) + end + + it 'fails to update' do + visit edit_path(model_name: 'fan', id: fan.id) + select("Fanship ##{fanships[0].id}", from: 'Fanships') + expect { click_button 'Save' }.to raise_error ActiveRecord::RecordNotFound + end + end + end + + describe 'via remote-sourced field' do + before do + RailsAdmin.config Fan do + field :name + field :fanships do + associated_collection_cache_all false + end + end + end + + it 'allows update', js: true do + visit edit_path(model_name: 'fan', id: fan.id) + find('input.ra-multiselect-search').set('F') + find('.ra-multiselect-collection option', text: "Fanship ##{fanships[0].id}").select_option + find('.ra-multiselect-collection option', text: "Fanship ##{fanships[1].id}").select_option + find('.ra-multiselect-item-add').click + click_button 'Save' + is_expected.to have_content 'Fan successfully updated' + expect(fan.reload.fanships.map(&:team_id)).to match_array fanships.map(&:team_id)[0..1] + end + end + + describe 'via nested field' do + let!(:team) { FactoryBot.create :team } + let!(:fanships) { FactoryBot.create_list(:fanship, 2, fan: fan) } + before do + RailsAdmin.config NestedFan do + field :name + field :fanships + end + end + + it 'allows update' do + visit edit_path(model_name: 'nested_fan', id: fan.id) + select(team.name, from: 'nested_fan_fanships_attributes_0_team_id') + fill_in 'nested_fan_fanships_attributes_1_since', with: '2020-01-23' + click_button 'Save' + is_expected.to have_content 'Nested fan successfully updated' + expect(fan.fanships[0].team).to eq team + expect(fan.fanships[1].since).to eq Date.new(2020, 1, 23) + end + end + end end diff --git a/spec/integration/fields/has_one_association_spec.rb b/spec/integration/fields/has_one_association_spec.rb index b2ee3f499d..8834921f13 100644 --- a/spec/integration/fields/has_one_association_spec.rb +++ b/spec/integration/fields/has_one_association_spec.rb @@ -1,20 +1,14 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'HasOneAssociation field', type: :request do subject { page } - describe 'with inverse_of option' do - it 'adds a related id to the belongs_to create team link' do - @player = FactoryBot.create :player - visit edit_path(model_name: 'player', id: @player.id) - is_expected.to have_selector("a[data-link='/admin/team/new?associations%5Bplayers%5D=#{@player.id}&modal=true']") - end - - it 'adds a related id to the has_many create team link' do - @team = FactoryBot.create :team - visit edit_path(model_name: 'team', id: @team.id) - is_expected.to have_selector("a[data-link='/admin/player/new?associations%5Bteam%5D=#{@team.id}&modal=true']") - end + it 'adds a related id to the has_one create draft link' do + @player = FactoryBot.create :player + visit edit_path(model_name: 'player', id: @player.id) + is_expected.to have_selector("a[data-link='/admin/draft/new?draft%5Bplayer_id%5D=#{@player.id}&modal=true']") end context 'on create' do @@ -28,31 +22,53 @@ end it 'creates an object with correct associations' do - post new_path(model_name: 'player', player: {name: 'Jackie Robinson', number: 42, position: 'Second baseman', draft_id: @draft.id}) - @player = RailsAdmin::AbstractModel.new('Player').all.to_a.detect { |player| player.name == 'Jackie Robinson' } + visit new_path(model_name: 'player') + fill_in 'Name', with: 'Jackie Robinson' + fill_in 'Number', with: @draft.player.number + 1 + select("Draft ##{@draft.id}", from: 'Draft') + click_button 'Save' + is_expected.to have_content 'Player successfully created' + @player = Player.where(name: 'Jackie Robinson').first @draft.reload expect(@player.draft).to eq(@draft) end + + context 'with default_value' do + before do + id = @draft.id + RailsAdmin.config Player do + configure :draft do + default_value id + end + end + end + + it 'shows the value as selected' do + visit new_path(model_name: 'player') + expect(find('select#player_draft_id').value).to eq @draft.id.to_s + end + end end context 'on update' do before do - @player = FactoryBot.create :player - @draft = FactoryBot.create :draft - @number = @draft.player.number + 1 # to avoid collision - put edit_path(model_name: 'player', id: @player.id, player: {name: 'Jackie Robinson', draft_id: @draft.id, number: @number, position: 'Second baseman'}) - @player.reload + @drafts = FactoryBot.create_list :draft, 2 + @player = FactoryBot.create :player, draft: @drafts[0] + visit edit_path(model_name: 'player', id: @player.id) end - it 'updates an object with correct attributes' do - expect(@player.name).to eq('Jackie Robinson') - expect(@player.number).to eq(@number) - expect(@player.position).to eq('Second baseman') + it 'updates an object with correct associations' do + select("Draft ##{@drafts[1].id}", from: 'Draft') + click_button 'Save' + @player.reload + expect(@player.draft).to eq(@drafts[1]) end - it 'updates an object with correct associations' do - @draft.reload - expect(@player.draft).to eq(@draft) + it 'clears the current selection' do + select('', from: 'Draft') + click_button 'Save' + @player.reload + expect(@player.draft).to be nil end end @@ -68,50 +84,6 @@ end end - describe 'nested form' do - it 'works', js: true do - @record = FactoryBot.create :field_test - visit edit_path(model_name: 'field_test', id: @record.id) - - find('#field_test_comment_attributes_field .add_nested_fields').click - fill_in 'field_test_comment_attributes_content', with: 'nested comment content' - - # trigger click via JS, workaround for instability in CI - execute_script %($('button[name="_save"]').trigger('click');) - is_expected.to have_content('Field test successfully updated') - - @record.reload - expect(@record.comment.content.strip).to eq('nested comment content') - end - - it 'is optional' do - @record = FactoryBot.create :field_test - visit edit_path(model_name: 'field_test', id: @record.id) - click_button 'Save' - @record.reload - expect(@record.comment).to be_nil - end - - context 'when XSS attack is attempted', js: true do - it 'does not break on adding a new item' do - allow(I18n).to receive(:t).and_call_original - expect(I18n).to receive(:t).with('admin.form.new_model', name: 'Comment').and_return('') - @record = FactoryBot.create :field_test - visit edit_path(model_name: 'field_test', id: @record.id) - find('#field_test_comment_attributes_field .add_nested_fields').click - end - - it 'does not break on adding an existing item' do - RailsAdmin.config Comment do - object_label_method :content - end - @record = FactoryBot.create :field_test - FactoryBot.create :comment, content: '', commentable: @record - visit edit_path(model_name: 'field_test', id: @record.id) - end - end - end - context 'with custom primary_key option' do let(:user) { FactoryBot.create :managing_user } let!(:team) { FactoryBot.create(:managed_team) } @@ -122,10 +94,11 @@ end end - it "allows update" do + it 'allows update' do visit edit_path(model_name: 'managing_user', id: user.id) select(team.name, from: 'Team') click_button 'Save' + is_expected.to have_content 'Managing user successfully updated' expect(ManagingUser.first.team).to eq team end @@ -136,16 +109,87 @@ end end - it "allows update", js: true do + it 'allows update', js: true do visit edit_path(model_name: 'managing_user', id: user.id) find('input.ra-filtering-select-input').set('T') - page.execute_script("$('input.ra-filtering-select-input').trigger('focus')") - page.execute_script("$('input.ra-filtering-select-input').trigger('keydown')") + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') - page.execute_script %{$('ul.ui-autocomplete li.ui-menu-item a:contains("#{team.name}")').trigger('mouseenter').click()} + page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("#{team.name}")).click()} click_button 'Save' + is_expected.to have_content 'Managing user successfully updated' expect(ManagingUser.first.team).to eq team end end end + + context 'with composite foreign keys', composite_primary_keys: true do + let(:fan) { FactoryBot.create(:fan) } + let!(:fanship) { FactoryBot.create(:fanship, fan: fan) } + + describe 'via default field' do + before do + RailsAdmin.config Fan do + field :name + field :fanship + end + end + + it 'allows create' do + visit new_path(model_name: 'fan') + fill_in 'Name', with: 'someone' + select("Fanship ##{fanship.id}", from: 'Fanship') + click_button 'Save' + is_expected.to have_content 'Fan successfully created' + expect(Fan.where(name: 'someone').first.fanship.team_id).to eq fanship.team_id + end + + it 'shows the current selection' do + visit edit_path(model_name: 'fan', id: fanship.fan_id) + is_expected.to have_select('Fanship', selected: "Fanship ##{fanship.id}") + end + end + + describe 'via remote-sourced field' do + before do + RailsAdmin.config Fan do + field :name + field :fanship do + associated_collection_cache_all false + end + end + end + + it 'allows create', js: true do + visit new_path(model_name: 'fan') + fill_in 'Name', with: 'someone' + find('.fanship_field input.ra-filtering-select-input').set(fanship.fan_id) + page.execute_script("document.querySelector('.fanship_field input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") + expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') + page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Fanship ##{fanship.id}")).click()} + click_button 'Save' + is_expected.to have_content 'Fan successfully created' + expect(Fan.where(name: 'someone').first.fanship.team_id).to eq fanship.team_id + end + end + + describe 'via nested field' do + let!(:team) { FactoryBot.create :team } + before do + RailsAdmin.config NestedFan do + field :name + field :fanship + end + end + + it 'allows update' do + visit edit_path(model_name: 'nested_fan', id: fanship.fan_id) + select(team.name, from: 'Team') + fill_in 'Since', with: '2020-01-23' + click_button 'Save' + is_expected.to have_content 'Nested fan successfully updated' + expect(fan.fanship.team).to eq team + expect(fan.fanship.since).to eq Date.new(2020, 1, 23) + end + end + end end diff --git a/spec/integration/fields/hidden_spec.rb b/spec/integration/fields/hidden_spec.rb index 54426da477..8ffc5e9565 100644 --- a/spec/integration/fields/hidden_spec.rb +++ b/spec/integration/fields/hidden_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'Hidden field', type: :request do @@ -39,5 +41,14 @@ it 'does not show help block' do is_expected.not_to have_xpath("id('player_name')/../p[@class='help-block']") end + + it 'submits the field value' do + visit new_path(model_name: 'player') + find("#player_name[type=hidden][value='username@example.com']", visible: false).set('someone@example.com') + fill_in 'Number', with: 1 + click_button 'Save' + is_expected.to have_content('Player successfully created') + expect(Player.first.name).to eq 'someone@example.com' + end end end diff --git a/spec/integration/fields/multiple_active_storage_spec.rb b/spec/integration/fields/multiple_active_storage_spec.rb new file mode 100644 index 0000000000..cfb72c8a9e --- /dev/null +++ b/spec/integration/fields/multiple_active_storage_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'MultipleActiveStorage field', type: :request, active_record: true do + subject { page } + before do + RailsAdmin.config FieldTest do + edit do + field :active_storage_assets + end + end + # To suppress 'SQLite3::BusyException: database is locked' exception + @original = page.driver.browser.url_blacklist # rubocop:disable Naming/InclusiveLanguage + page.driver.browser.url_blacklist = [%r{/rails/active_storage/representations}] # rubocop:disable Naming/InclusiveLanguage + end + after { page.driver.browser.url_blacklist = @original } # rubocop:disable Naming/InclusiveLanguage + + it 'supports uploading multiple files', js: true do + visit new_path(model_name: 'field_test') + attach_file 'Active storage assets', [file_path('test.jpg'), file_path('test.png')] + click_button 'Save' + is_expected.to have_content 'Field test successfully created' + expect(FieldTest.first.active_storage_assets.map { |image| image.filename.to_s }).to match_array ['test.jpg', 'test.png'] + end + + context 'when working with existing files' do + let(:field_test) { FactoryBot.create(:field_test, active_storage_assets: ['test.jpg', 'test.png'].map { |img| {io: File.open(file_path(img)), filename: img} }) } + + it 'supports appending a file', js: true do + visit edit_path(model_name: 'field_test', id: field_test.id) + attach_file 'Active storage assets', [file_path('test.gif')] + click_button 'Save' + is_expected.to have_content 'Field test successfully updated' + field_test.reload + expect(field_test.active_storage_assets.map { |image| image.filename.to_s }).to eq ['test.jpg', 'test.png', 'test.gif'] + end + + it 'supports deleting a file', js: true do + visit edit_path(model_name: 'field_test', id: field_test.id) + click_link "Delete 'Active storage assets' #1" + click_button 'Save' + is_expected.to have_content 'Field test successfully updated' + field_test.reload + expect(field_test.active_storage_assets.map { |image| image.filename.to_s }).to eq ['test.png'] + end + end + + describe 'direct upload', js: true do + let(:field_test) { FactoryBot.create :field_test } + before do + RailsAdmin.config FieldTest do + edit do + configure(:active_storage_assets) { direct true } + end + end + end + + it 'works' do + visit edit_path(model_name: 'field_test', id: field_test.id) + attach_file 'Active storage assets', [file_path('test.jpg')] + expect_any_instance_of(ActiveStorage::DirectUploadsController).to receive(:create).and_call_original + click_button 'Save' + expect(page).to have_content 'Field test successfully updated' + field_test.reload + expect(field_test.active_storage_assets.map { |image| image.filename.to_s }).to eq ['test.jpg'] + end + end +end diff --git a/spec/integration/fields/multiple_carrierwave_spec.rb b/spec/integration/fields/multiple_carrierwave_spec.rb index b620a3ad39..fc1ab1178c 100644 --- a/spec/integration/fields/multiple_carrierwave_spec.rb +++ b/spec/integration/fields/multiple_carrierwave_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'MultipleCarrierwave field', type: :request, active_record: true do @@ -10,10 +12,11 @@ end end - it 'supports uploading multiple files' do + it 'supports uploading multiple files', js: true do visit new_path(model_name: 'field_test') - attach_file "Carrierwave assets", [file_path('test.jpg'), file_path('test.png')] + attach_file 'Carrierwave assets', [file_path('test.jpg'), file_path('test.png')] click_button 'Save' + is_expected.to have_content 'Field test successfully created' expect(FieldTest.first.carrierwave_assets.map { |image| File.basename(image.url) }).to match_array ['test.jpg', 'test.png'] end @@ -22,25 +25,29 @@ it 'supports appending a file', js: true do visit edit_path(model_name: 'field_test', id: field_test.id) - attach_file "Carrierwave assets", [file_path('test.gif')] + attach_file 'Carrierwave assets', [file_path('test.gif')] click_button 'Save' + is_expected.to have_content 'Field test successfully updated' field_test.reload expect(field_test.carrierwave_assets.map { |image| File.basename(image.url) }).to eq ['test.jpg', 'test.png', 'test.gif'] end it 'supports deleting a file', js: true do visit edit_path(model_name: 'field_test', id: field_test.id) - click_link 'Delete carrierwave assets #1' + click_link "Delete 'Carrierwave assets' #1" click_button 'Save' + is_expected.to have_content 'Field test successfully updated' field_test.reload expect(field_test.carrierwave_assets.map { |image| File.basename(image.url) }).to eq ['test.png'] end it 'supports reordering files', js: true do visit edit_path(model_name: 'field_test', id: field_test.id) - page.execute_script File.read(File.expand_path('../../../support/jquery.simulate.drag-sortable.js', __FILE__)) - page.execute_script %{$(".ui-sortable-handle:first-child").simulateDragSortable({ move: 1});} + page.execute_script File.read(File.expand_path('../../../vendor/assets/javascripts/rails_admin/jquery3.js', __dir__)) + page.execute_script File.read(File.expand_path('../../support/jquery.simulate.drag-sortable.js', __dir__)) + page.execute_script %{$(".ui-sortable-handle:first-child").simulateDragSortable({move: 1});} click_button 'Save' + is_expected.to have_content 'Field test successfully updated' field_test.reload expect(field_test.carrierwave_assets.map { |image| File.basename(image.url) }).to eq ['test.png', 'test.jpg'] end diff --git a/spec/integration/fields/multiple_file_upload_spec.rb b/spec/integration/fields/multiple_file_upload_spec.rb new file mode 100644 index 0000000000..5446924246 --- /dev/null +++ b/spec/integration/fields/multiple_file_upload_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'MultipleFileUpload field', type: :request do + subject { page } + + before do + RailsAdmin.config FieldTest do + field :string_field, :multiple_file_upload do + attachment do + delete_value { value } + def resource_url(_thumb = false) + value + end + end + delete_method 'boolean_field' + reorderable true + def value + bindings[:object].safe_send(name)&.split + end + end + end + end + let(:field_test) { FactoryBot.create :field_test, string_field: 'http://localhost/1.jpg http://localhost/2.jpg' } + + it 'supports deletion', js: true do + visit edit_path(model_name: 'field_test', id: field_test.id) + click_link "Delete 'String field' #1" + expect(page.all(:css, '[name="field_test[boolean_field][]"]:checked', visible: false).map(&:value)).to eq %w[http://localhost/1.jpg] + end + + it 'shows a inline preview', js: true do + visit new_path(model_name: 'field_test') + attach_file 'String field', file_path('test.jpg') + is_expected.to have_selector('#field_test_string_field_field img.preview') + end +end diff --git a/spec/integration/fields/paperclip_spec.rb b/spec/integration/fields/paperclip_spec.rb index fd2d232693..230c34bbd1 100644 --- a/spec/integration/fields/paperclip_spec.rb +++ b/spec/integration/fields/paperclip_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'Paperclip field', type: :request do diff --git a/spec/integration/fields/polymorphic_assosiation_spec.rb b/spec/integration/fields/polymorphic_assosiation_spec.rb index 5ffdc10e47..c2ed1bac76 100644 --- a/spec/integration/fields/polymorphic_assosiation_spec.rb +++ b/spec/integration/fields/polymorphic_assosiation_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'PolymorphicAssociation field', type: :request do @@ -9,11 +11,11 @@ visit new_path(model_name: 'comment') select 'Player', from: 'comment[commentable_type]' find('input.ra-filtering-select-input').set('Rob') - page.execute_script("$('input.ra-filtering-select-input').trigger('focus')") - page.execute_script("$('input.ra-filtering-select-input').trigger('keydown')") + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') - page.execute_script %{$('ul.ui-autocomplete li.ui-menu-item a:contains("Jackie Robinson")').trigger('mouseenter').click()} + page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Jackie Robinson")).click()} click_button 'Save' + is_expected.to have_content 'Comment successfully created' expect(Comment.first.commentable).to eq @players[0] end @@ -24,26 +26,94 @@ expect(@comment.commentable_type).to eq 'Ball' expect(@comment.commentable).to eq @hardball end - end - context 'on update' do - before :each do - @team = FactoryBot.create :team - @comment = FactoryBot.create :comment, commentable: @team + it 'clears the selected id on type change', js: true do + @players = ['Jackie Robinson', 'Rob Wooten'].map { |name| FactoryBot.create :player, name: name } + visit new_path(model_name: 'comment') + select 'Player', from: 'comment[commentable_type]' + find('input.ra-filtering-select-input').set('Rob') + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") + expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') + page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Jackie Robinson")).click()} + select 'Team', from: 'comment[commentable_type]' + expect(find('#comment_commentable_id', visible: false).value).to eq '' end - it 'is editable' do - visit edit_path(model_name: 'comment', id: @comment.id) + context 'when the associated model is declared in a two-level namespace' do + it 'successfully saves the record', js: true do + polymorphic_association_tests = ['Jackie Robinson', 'Rob Wooten'].map do |name| + FactoryBot.create(:two_level_namespaced_polymorphic_association_test, name: name) + end + + visit new_path(model_name: 'comment') - is_expected.to have_selector('select#comment_commentable_type') - is_expected.to have_selector('select#comment_commentable_id') + select 'Polymorphic association test', from: 'comment[commentable_type]' + find('input.ra-filtering-select-input').set('Rob') + + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") + expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') + + page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Jackie Robinson")).click()} + click_button 'Save' + is_expected.to have_content 'Comment successfully created' + expect(Comment.first.commentable).to eq polymorphic_association_tests.first + end + end + end + + context 'on update' do + let(:team) { FactoryBot.create :team, name: 'Los Angeles Dodgers' } + let(:comment) { FactoryBot.create :comment, commentable: team } + let!(:players) { ['Jackie Robinson', 'Rob Wooten', 'Scott Hairston'].map { |name| FactoryBot.create :player, name: name } } + + it 'is editable', js: true do + visit edit_path(model_name: 'comment', id: comment.id) + expect(find('select#comment_commentable_type').value).to eq 'Team' + expect(find('select#comment_commentable_id', visible: false).value).to eq team.id.to_s + find('input.ra-filtering-select-input').set('Los') + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") + expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') + expect(all('ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to eq ['Los Angeles Dodgers'] + select 'Player', from: 'comment[commentable_type]' + find('input.ra-filtering-select-input').set('Rob') + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") + expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') + expect(all('ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to eq ['Rob Wooten', 'Jackie Robinson'] + page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Jackie Robinson")).click()} + click_button 'Save' + is_expected.to have_content 'Comment successfully updated' + expect(comment.reload.commentable).to eq players[0] end it 'is visible in the owning end' do - visit edit_path(model_name: 'team', id: @team.id) + visit edit_path(model_name: 'team', id: team.id) is_expected.to have_selector('select#team_comment_ids') end + + context 'with records in different models share the same id', js: true do + let!(:players) { [FactoryBot.create(:player, id: team.id, name: 'Jackie Robinson')] } + + it 'clears the selected id on type change', js: true do + visit edit_path(model_name: 'comment', id: comment.id) + select 'Player', from: 'comment[commentable_type]' + click_button 'Save' + is_expected.to have_content 'Comment successfully updated' + expect(comment.reload.commentable).to eq nil + end + + it 'updates correctly', js: true do + visit edit_path(model_name: 'comment', id: comment.id) + select 'Player', from: 'comment[commentable_type]' + find('input.ra-filtering-select-input').set('Rob') + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") + expect(page).to have_selector('ul.ui-autocomplete li.ui-menu-item a') + page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Jackie Robinson")).click()} + click_button 'Save' + is_expected.to have_content 'Comment successfully updated' + expect(comment.reload.commentable).to eq players[0] + end + end end context 'on show' do diff --git a/spec/integration/fields/serialized_spec.rb b/spec/integration/fields/serialized_spec.rb index 92428732e5..93b43bfbe7 100644 --- a/spec/integration/fields/serialized_spec.rb +++ b/spec/integration/fields/serialized_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'Serialized field', type: :request do @@ -22,7 +24,7 @@ end it 'saves the serialized data' do - expect(@user.roles).to eq(%w(admin user)) + expect(@user.roles).to eq(%w[admin user]) end end diff --git a/spec/integration/fields/shrine_spec.rb b/spec/integration/fields/shrine_spec.rb index 98dd74374e..1586c68e9e 100644 --- a/spec/integration/fields/shrine_spec.rb +++ b/spec/integration/fields/shrine_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'Shrine field', type: :request, active_record: true do @@ -13,7 +15,7 @@ it 'supports caching an uploaded file', js: true do visit new_path(model_name: 'field_test') - attach_file "Shrine asset", file_path('test.jpg') + attach_file 'Shrine asset', file_path('test.jpg') fill_in 'field_test[string_field]', with: 'Invalid' click_button 'Save' expect(page).to have_content 'Field test failed to be created' diff --git a/spec/integration/fields/simple_mde_spec.rb b/spec/integration/fields/simple_mde_spec.rb index 6cbe1391e9..660a5c6a0d 100644 --- a/spec/integration/fields/simple_mde_spec.rb +++ b/spec/integration/fields/simple_mde_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'SimpleMDE field', type: :request do diff --git a/spec/integration/fields/time_spec.rb b/spec/integration/fields/time_spec.rb new file mode 100644 index 0000000000..451405c2ea --- /dev/null +++ b/spec/integration/fields/time_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Time field', type: :request, active_record: true do + subject { page } + before do + RailsAdmin.config FieldTest do + edit do + field :time_field + end + end + end + + describe 'filtering' do + let!(:field_tests) do + [FactoryBot.create(:field_test, time_field: DateTime.new(2000, 1, 1, 3, 45)), + FactoryBot.create(:field_test, time_field: DateTime.new(2000, 1, 1, 4, 45))] + end + + it 'correctly returns a record' do + visit index_path(model_name: 'field_test', f: {time_field: {'1' => {v: [nil, '2000-01-01T04:00:00', nil], o: 'between'}}}) + is_expected.to have_content '1 field test' + is_expected.to have_content field_tests[1].id + end + + it 'does not break when the condition is not filled' do + visit index_path(model_name: 'field_test', f: {time_field: {'1' => {v: [nil, '', ''], o: 'between'}}}) + is_expected.to have_content '2 field test' + end + + context 'with server timezone changed' do + around do |example| + original = Time.zone + Time.zone = ActiveSupport::TimeZone.new('Central Time (US & Canada)') + example.run + Time.zone = original + end + + it 'correctly returns a record' do + visit index_path(model_name: 'field_test', f: {time_field: {'1' => {v: [nil, '2000-01-01T22:00:00', nil], o: 'between'}}}) + is_expected.to have_content '1 field test' + is_expected.to have_content field_tests[1].id + end + end + end + + context 'on create' do + it 'is initially blank' do + visit new_path(model_name: 'field_test') + expect(find('[name="field_test[time_field]"]', visible: false).value).to be_blank + end + + it 'persists the value' do + visit new_path(model_name: 'field_test') + find('[name="field_test[time_field]"]', visible: false).set('2000-01-01T03:45:00') + click_button 'Save' + expect(FieldTest.count).to eq 1 + expect(FieldTest.first.time_field).to eq DateTime.new(2000, 1, 1, 3, 45) + end + + it 'ignores the date part' do + visit new_path(model_name: 'field_test') + find('[name="field_test[time_field]"]', visible: false).set('2021-01-02T03:45:00') + click_button 'Save' + expect(FieldTest.first.time_field).to eq DateTime.new(2000, 1, 1, 3, 45) + end + end + + context 'on update' do + let(:field_test) { FactoryBot.create :field_test, time_field: DateTime.new(2021, 1, 2, 3, 45) } + + it 'updates the value' do + visit edit_path(model_name: 'field_test', id: field_test.id) + expect(find('[name="field_test[time_field]"]', visible: false).value).to eq '2000-01-01T03:45:00' + find('[name="field_test[time_field]"]', visible: false).set('2021-02-03T04:55:00') + click_button 'Save' + field_test.reload + expect(field_test.time_field).to eq DateTime.new(2000, 1, 1, 4, 55) + end + end + + context 'with server timezone changed' do + let(:field_test) { FactoryBot.create :field_test, time_field: DateTime.new(2000, 1, 1, 6, 45) } + + around do |example| + original = Time.zone + Time.zone = ActiveSupport::TimeZone.new('Central Time (US & Canada)') + example.run + Time.zone = original + end + + it 'treats the datetime set by the browser as local time' do + visit edit_path(model_name: 'field_test', id: field_test.id) + find('[name="field_test[time_field]"]', visible: false).set('2000-01-01T04:55:00') + click_button 'Save' + field_test.reload + expect(field_test.time_field.iso8601).to eq '2000-01-01T04:55:00-06:00' + end + + it 'is not altered by just saving untouched' do + visit edit_path(model_name: 'field_test', id: field_test.id) + expect(find('[name="field_test[time_field]"]', visible: false).value).to eq '2000-01-01T00:45:00' + click_button 'Save' + expect { field_test.reload }.not_to change(field_test, :time_field) + end + end +end diff --git a/spec/integration/fields/wysihtml5_spec.rb b/spec/integration/fields/wysihtml5_spec.rb index 4e1b63675f..0e120d9414 100644 --- a/spec/integration/fields/wysihtml5_spec.rb +++ b/spec/integration/fields/wysihtml5_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'Wysihtml5 field', type: :request do @@ -25,6 +27,6 @@ end visit new_path(model_name: 'draft') - is_expected.to have_selector("textarea#draft_notes[data-richtext=\"bootstrap-wysihtml5\"][data-options]") + is_expected.to have_selector('textarea#draft_notes[data-richtext="bootstrap-wysihtml5"][data-options]') end end diff --git a/spec/integration/history/rails_admin_history_spec.rb b/spec/integration/history/rails_admin_history_spec.rb deleted file mode 100644 index 496fd2efdb..0000000000 --- a/spec/integration/history/rails_admin_history_spec.rb +++ /dev/null @@ -1,87 +0,0 @@ -require 'spec_helper' - -RSpec.describe 'RailsAdmin History', type: :request, active_record: true do - describe 'model history fetch' do - before :each do - RailsAdmin::History.delete_all - @model = RailsAdmin::AbstractModel.new('Player') - player = FactoryBot.create :player - 30.times do |i| - player.number = i - RailsAdmin::History.create_history_item "change #{i}", player, @model, nil - end - end - - it 'fetches on page of history' do - histories = RailsAdmin::History.history_for_model @model, nil, false, false, false, nil, 20 - expect(histories.total_count).to eq(30) - expect(histories.count).to eq(20) - end - - it 'respects RailsAdmin::Config.default_items_per_page' do - RailsAdmin.config.default_items_per_page = 15 - histories = RailsAdmin::History.history_for_model @model, nil, false, false, false, nil - expect(histories.total_count).to eq(30) - expect(histories.count).to eq(15) - end - - context 'with Kaminari' do - before do - Kaminari.config.page_method_name = :per_page_kaminari - @paged = RailsAdmin::History.page(1) - end - - after do - Kaminari.config.page_method_name = :page - end - - it "supports pagination when Kaminari's page_method_name is customized" do - expect(RailsAdmin::History).to receive(:per_page_kaminari).twice.and_return(@paged) - RailsAdmin::History.history_for_model @model, nil, false, false, false, nil - RailsAdmin::History.history_for_object @model, Player.first, nil, false, false, false, nil - end - end - - context 'GET admin/history/@model' do - before :each do - RailsAdmin.config do |c| - c.audit_with :history - end - - visit history_index_path(@model) - end - - # https://github.com/sferik/rails_admin/issues/362 - # test that no link uses the "wildcard route" with the history - # controller and for_model method - it "does not use the 'wildcard route'" do - expect(page).to have_selector("a[href*='all=true']") # make sure we're fully testing pagination - expect(page).to have_no_selector("a[href^='/rails_admin/history/for_model']") - end - - context 'with a lot of histories' do - before :each do - player = Player.create(team_id: -1, number: -1, name: 'Player 1') - 101.times do |i| - player.number = i - RailsAdmin::History.create_history_item "change #{i}", player, @model, nil - end - end - - it 'gets latest ones' do - expect(RailsAdmin::History.latest.count).to eq(100) - end - - it 'gets latest ones orderly' do - latest = RailsAdmin::History.latest - expect(latest.first.message).to eq('change 100') - expect(latest.last.message).to eq('change 1') - end - - it 'renders a XHR request successfully' do - get history_index_path(@model, page: 2), xhr: true - end - end - end - end -end diff --git a/spec/integration/rails_admin_spec.rb b/spec/integration/rails_admin_spec.rb index 6bc676a2c2..8040dcc826 100644 --- a/spec/integration/rails_admin_spec.rb +++ b/spec/integration/rails_admin_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin, type: :request do @@ -28,18 +30,73 @@ describe 'html head' do before { visit dashboard_path } - # Note: the [href^="/asset... syntax matches the start of a value. The reason + # NOTE: the [href^="/asset... syntax matches the start of a value. The reason # we just do that is to avoid being confused by rails' asset_ids. it 'loads stylesheets in header' do - is_expected.to have_selector('head link[href^="/assets/rails_admin/rails_admin"][href$=".css"]', visible: false) + case RailsAdmin.config.asset_source + when :sprockets + is_expected.to have_selector('head link[href^="/assets/rails_admin"][href$=".css"]', visible: false) + when :webpacker + is_expected.to have_no_selector('head link[href~="rails_admin"][href$=".css"]', visible: false) + end end it 'loads javascript files in body' do - is_expected.to have_selector('head script[src^="/assets/rails_admin/rails_admin"][src$=".js"]', visible: false) + case RailsAdmin.config.asset_source + when :sprockets + is_expected.to have_selector('head script[src^="/assets/rails_admin"][src$=".js"]', visible: false) + when :webpacker + is_expected.to have_selector('head script[src^="/packs-test/js/rails_admin"][src$=".js"]', visible: false) + end + end + end + + describe 'custom theming' do + before { visit dashboard_path } + + if CI_ASSET == :sprockets + it 'applies the style overridden by assets in the application', js: true do + expect(find('.navbar-brand small').style('opacity')).to eq({'opacity' => '0.99'}) + end + end + end + + describe 'navbar css class' do + it 'is set by default' do + expect(RailsAdmin.config.navbar_css_classes).to eq(%w[navbar-dark bg-primary border-bottom]) + end + + it 'can be configured' do + RailsAdmin.config do |config| + config.navbar_css_classes = %w[navbar-light border-bottom] + end + visit dashboard_path + is_expected.to have_css('nav.navbar.navbar-light.border-bottom') end end - describe '_current_user' do # https://github.com/sferik/rails_admin/issues/549 + describe 'sidebar navigation', js: true do + it 'is collapsible' do + visit dashboard_path + is_expected.to have_css('.sidebar .nav-link', text: 'Players') + click_button 'Navigation' + is_expected.to have_css('.sidebar .btn-toggle.collapsed') + is_expected.not_to have_css('.sidebar #navigation.show') + is_expected.not_to have_css('.sidebar .nav-link', text: 'Players') + end + + it 'persists over a page transition' do + visit dashboard_path + click_button 'Navigation' + is_expected.to have_css('.sidebar .btn-toggle.collapsed') + is_expected.not_to have_css('.sidebar #navigation.show') + find('.player_links .show a').trigger('click') + is_expected.to have_content 'List of Players' + is_expected.not_to have_css('.sidebar .nav-link', text: 'Players') + end + end + + describe '_current_user' do # https://github.com/railsadminteam/rails_admin/issues/549 it 'is accessible from the list view' do RailsAdmin.config Player do list do @@ -66,7 +123,7 @@ describe 'secondary navigation' do it 'has Gravatar image' do visit dashboard_path - is_expected.to have_selector('ul.navbar-right img[src*="gravatar.com"]') + is_expected.to have_selector('ul.navbar-nav img[src*="gravatar.com"]') end it "does not show Gravatar when user doesn't have email method" do @@ -88,9 +145,9 @@ is_expected.to have_content 'Log out' end - it 'has label-danger class on log out link' do + it 'has bg-danger class on log out link' do visit dashboard_path - is_expected.to have_selector('.label-danger') + is_expected.to have_selector('.bg-danger') end it 'has links for actions which are marked as show_in_navigation' do @@ -125,11 +182,37 @@ it 'is enforced' do visit new_path(model_name: 'league') fill_in 'league[name]', with: 'National league' - find('input[name="authenticity_token"]', visible: false).set("invalid token") + find('input[name="authenticity_token"]', visible: false).set('invalid token') expect { click_button 'Save' }.to raise_error ActionController::InvalidAuthenticityToken end end + describe 'Turbo Drive', js: true do + let(:player) { FactoryBot.create :player } + + it 'does not trigger JS errors by going away from and back to RailsAdmin' do + visit show_path(model_name: 'player', id: player.id) + click_link 'Show in app' + click_link 'Back to admin' + is_expected.to have_content 'Details for Player' + end + + it 'triggers rails_admin.dom_ready right after a validation error' do + visit edit_path(model_name: 'player', id: player.id) + fill_in 'player[name]', with: 'on steroids' + find_button('Save').trigger 'click' + is_expected.to have_content 'Player failed to be updated' + is_expected.to have_css '.filtering-select[data-input-for="player_team_id"]' + end + end + + describe 'dom_ready events', js: true do + it 'trigger properly' do + visit dashboard_path + expect(evaluate_script('domReadyTriggered')).to match_array %w[plainjs/dot jquery/dot] + end + end + context 'with invalid model name' do it "redirects to dashboard and inform the user the model wasn't found" do visit '/admin/whatever' diff --git a/spec/integration/widgets/datetimepicker_spec.rb b/spec/integration/widgets/datetimepicker_spec.rb new file mode 100644 index 0000000000..ec715b9e23 --- /dev/null +++ b/spec/integration/widgets/datetimepicker_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Datetimepicker widget', type: :request, js: true do + subject { page } + + before do + RailsAdmin.config FieldTest do + edit do + field :datetime_field + end + end + end + + it 'is initially blank' do + visit new_path(model_name: 'field_test') + expect(find('[name="field_test[datetime_field]"]', visible: false).value).to be_blank + expect(find('[name="field_test[datetime_field]"] + input').value).to be_blank + end + + it 'populates the value selected by the Datetime picker into the hidden_field' do + visit new_path(model_name: 'field_test') + is_expected.to have_css '.form-control.flatpickr-input', visible: false + page.execute_script <<-JS + document.querySelector('#field_test_datetime_field')._flatpickr.setDate('2015-10-08 14:00:00'); + JS + expect(find('#field_test_datetime_field + input').value).to eq 'October 08, 2015 14:00' + expect(find('[name="field_test[datetime_field]"]', visible: false).value).to eq '2015-10-08T14:00:00' + end + + it 'populates the value entered in the text field into the hidden_field' do + visit new_path(model_name: 'field_test') + find('#field_test_datetime_field + input').set 'January 2, 2021 03:45' + expect(find('[name="field_test[datetime_field]"]', visible: false).value).to eq '2021-01-02T03:45:00' + expect(find('#field_test_datetime_field + input').value).to eq 'January 02, 2021 03:45' + end + + it 'works with a different format' do + RailsAdmin.config FieldTest do + edit do + configure(:datetime_field) { strftime_format '%Y-%m-%d' } + end + end + visit new_path(model_name: 'field_test') + find('#field_test_datetime_field + input').set '2021-01-02' + expect(find('[name="field_test[datetime_field]"]', visible: false).value).to eq '2021-01-02T00:00:00' + expect(find('#field_test_datetime_field + input').value).to eq '2021-01-02' + end + + it 'supports custom flatpickr_format' do + RailsAdmin.config FieldTest do + edit do + configure(:datetime_field) { flatpickr_format 'H\Hi\MS\S' } + end + end + visit new_path(model_name: 'field_test') + is_expected.to have_css '.form-control.flatpickr-input', visible: false + page.execute_script <<-JS + document.querySelector('#field_test_datetime_field')._flatpickr.setDate('2015-10-08 12:34:56'); + JS + expect(find('#field_test_datetime_field + input').value).to eq '12H34M56S' + end + + context 'with locale set' do + around(:each) do |example| + original = I18n.default_locale + I18n.default_locale = :fr + example.run + I18n.default_locale = original + end + + it 'shows and accepts the value in the given locale' do + visit new_path(model_name: 'field_test', field_test: {datetime_field: '2021-01-02T03:45:00'}) + expect(find('[name="field_test[datetime_field]"]', visible: false).value).to eq '2021-01-02T03:45:00' + expect(find('#field_test_datetime_field + input').value).to eq 'samedi 02 janvier 2021 03:45' + find('#field_test_datetime_field + input').set 'mercredi 03 février 2021 04:55' + expect(find('[name="field_test[datetime_field]"]', visible: false).value).to eq '2021-02-03T04:55:00' + end + end +end diff --git a/spec/integration/widgets/filter_box_spec.rb b/spec/integration/widgets/filter_box_spec.rb new file mode 100644 index 0000000000..27a2a89a35 --- /dev/null +++ b/spec/integration/widgets/filter_box_spec.rb @@ -0,0 +1,220 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Filter box widget', type: :request, js: true do + subject { page } + + it 'adds filters' do + RailsAdmin.config Player do + field :name + field :position + end + visit index_path(model_name: 'player') + is_expected.to have_no_css('#filters_box .filter') + click_link 'Add filter' + click_link 'Name' + within('#filters_box') do + is_expected.to have_css('.filter', count: 1) + is_expected.to have_css('.filter select[name^="f[name]"]') + end + click_link 'Add filter' + click_link 'Position' + within('#filters_box') do + is_expected.to have_css('.filter', count: 2) + is_expected.to have_css('.filter select[name^="f[position]"]') + end + end + + it 'removes filters' do + RailsAdmin.config Player do + field :name + field :position + end + visit index_path(model_name: 'player') + is_expected.to have_no_css('#filters_box .filter') + click_link 'Add filter' + click_link 'Name' + click_link 'Add filter' + click_link 'Position' + within('#filters_box') do + is_expected.to have_css('.filter', count: 2) + click_button 'Name' + is_expected.to have_no_css('.filter select[name^="f[name]"]') + click_button 'Position' + is_expected.to have_no_css('.filter') + end + end + + it 'hides redundant filter options for required fields' do + RailsAdmin.config Player do + list do + field :name do + required true + end + field :team + end + end + + visit index_path(model_name: 'player', f: {name: {'1' => {v: ''}}, team: {'2' => {v: ''}}}) + + within(:select, name: 'f[name][1][o]') do + expect(page.all('option').map(&:value)).to_not include('_present', '_blank') + end + + within(:select, name: 'f[team][2][o]') do + expect(page.all('option').map(&:value)).to include('_present', '_blank') + end + end + + it 'supports limiting filter operators' do + RailsAdmin.config Player do + list do + field :name do + filter_operators %w[is starts_with _present] + end + end + end + + visit index_path(model_name: 'player') + is_expected.to have_no_css('#filters_box .filter') + click_link 'Add filter' + click_link 'Name' + + within(:select, name: /f\[name\]\[\d+\]\[o\]/) do + expect(page.all('option').map(&:value)).to eq %w[is starts_with _present] + end + end + + it 'does not cause duplication when using browser back' do + RailsAdmin.config Player do + field :name + end + + visit index_path(model_name: 'player', f: {name: {'1' => {v: 'a'}}}) + find(%([href$="/admin/player/export"])).click + is_expected.to have_content 'Export Players' + page.go_back + is_expected.to have_content 'List of Players' + expect(all(:css, '#filters_box div.filter').count).to eq 1 + end + + describe 'for boolean field' do + before do + RailsAdmin.config FieldTest do + field :boolean_field + end + end + + it 'is filterable with true and false' do + visit index_path(model_name: 'field_test') + click_link 'Add filter' + click_link 'Boolean field' + within('#filters_box') do + expect(page.all('option').map(&:value)).to include('true', 'false') + end + end + end + + describe 'for date field' do + before do + RailsAdmin.config FieldTest do + field :date_field + end + end + + it 'populates the value selected by the Datetimepicker into the hidden_field' do + visit index_path(model_name: 'field_test') + click_link 'Add filter' + click_link 'Date field' + expect(find('[name^="f[date_field]"][name$="[v][]"]', match: :first, visible: false).value).to be_blank + page.execute_script <<-JS + document.querySelector('.form-control.date')._flatpickr.setDate('2015-10-08'); + JS + expect(find('[name^="f[date_field]"][name$="[v][]"]', match: :first, visible: false).value).to eq '2015-10-08T00:00:00' + end + end + + describe 'for datetime field' do + before do + RailsAdmin.config FieldTest do + field :datetime_field + end + end + + it 'populates the value selected by the Datetimepicker into the hidden_field' do + visit index_path(model_name: 'field_test') + click_link 'Add filter' + click_link 'Datetime field' + expect(find('[name^="f[datetime_field]"][name$="[v][]"]', match: :first, visible: false).value).to be_blank + page.execute_script <<-JS + document.querySelector('.form-control.datetime')._flatpickr.setDate('2015-10-08 14:00:00'); + JS + expect(find('[name^="f[datetime_field]"][name$="[v][]"]', match: :first, visible: false).value).to eq '2015-10-08T14:00:00' + end + end + + describe 'for enum field' do + before do + RailsAdmin.config Team do + field :color, :enum + end + end + + it 'supports multiple selection mode' do + visit index_path(model_name: 'team') + click_link 'Add filter' + click_link 'Color' + expect(all('#filters_box option').map(&:text)).to include 'white', 'black', 'red', 'green', 'blué' + find('.filter .switch-select .fa-plus').click + expect(find('#filters_box select')['multiple']).to be true + expect(find('#filters_box select')['name']).to match(/\[\]$/) + end + + context 'with the filter pre-populated' do + it 'does not break' do + visit index_path(model_name: 'team', f: {color: {'1' => {v: 'red'}}}) + is_expected.to have_css('.filter select[name^="f[color]"]') + expect(find('.filter select[name^="f[color]"]').value).to eq 'red' + expect(all('#filters_box option').map(&:text)).to include 'white', 'black', 'red', 'green', 'blué' + end + end + end + + describe 'for time field', active_record: true do + before do + RailsAdmin.config FieldTest do + field :time_field + end + end + + it 'populates the value selected by the Datetimepicker into the hidden_field' do + visit index_path(model_name: 'field_test') + click_link 'Add filter' + click_link 'Time field' + expect(find('[name^="f[time_field]"][name$="[v][]"]', match: :first, visible: false).value).to be_blank + page.execute_script <<-JS + document.querySelector('.form-control.datetime')._flatpickr.setDate('2000-01-01 14:00:00'); + JS + expect(find('[name^="f[time_field]"][name$="[v][]"]', match: :first, visible: false).value).to eq '2000-01-01T14:00:00' + end + end + + describe 'for has_one association field' do + before do + RailsAdmin.config Player do + field :draft do + searchable :college + end + end + end + + it 'is filterable' do + visit index_path(model_name: 'player') + click_link 'Add filter' + click_link 'Draft' + expect(page).to have_css '[name^="f[draft]"][name$="[o]"]' + expect(page).to have_css '[name^="f[draft]"][name$="[v]"]' + end + end +end diff --git a/spec/integration/widgets/filtering_multi_select_spec.rb b/spec/integration/widgets/filtering_multi_select_spec.rb index 41a82ea210..2ad21a16b6 100644 --- a/spec/integration/widgets/filtering_multi_select_spec.rb +++ b/spec/integration/widgets/filtering_multi_select_spec.rb @@ -1,46 +1,165 @@ +# frozen_string_literal: true + require 'spec_helper' -RSpec.describe 'FilteringMultiSelect widget', type: :request, js: true do +RSpec.describe 'Filtering multi-select widget', type: :request, js: true do subject { page } - describe 'Choose all button' do - let(:team) { FactoryBot.create :team } - let!(:players) { FactoryBot.create_list :player, 2 } - before do - RailsAdmin.config Team do - field :players - end + let(:team) { FactoryBot.create :team } + let!(:players) { ['Cory Burns', 'Leonys Martin', 'Matt Garza'].map { |name| FactoryBot.create :player, name: name } } + before do + RailsAdmin.config Team do + field :players + end + end + + context 'on create' do + before { visit new_path(model_name: 'team') } + + it 'is initially unset' do + expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Leonys Martin', 'Cory Burns', 'Matt Garza'] + expect(find('.ra-multiselect-selection').text).to be_empty + end + + it 'supports filtering' do + find('input.ra-multiselect-search').set('Alex') + page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))") + is_expected.to have_content 'No objects found' + find('input.ra-multiselect-search').set('Ma') + page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))") + is_expected.to have_content 'Leonys' + expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Leonys Martin', 'Matt Garza'] + end + + it 'sets ids of the selected items' do + find('.ra-multiselect-collection option', text: /Cory/).select_option + within('.ra-multiselect-center') { click_link 'Add new' } + expect(all('.ra-multiselect-selection option').map(&:text)).to match_array ['Cory Burns'] + expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Leonys Martin', 'Matt Garza'] + expect(all('#team_player_ids option', visible: false).select(&:selected?).map(&:value)).to match_array [players[0].id.to_s] + end + end + + context 'on update' do + let!(:player) { FactoryBot.create :player, team: team, name: 'Elvis Andrus' } + before { visit edit_path(model_name: 'team', id: team.id) } + + it 'additionally selects items' do + expect(all('#team_player_ids option', visible: false).select(&:selected?).map(&:value)).to match_array [player.id.to_s] + find('.ra-multiselect-collection option', text: /Cory/).select_option + within('.ra-multiselect-center') { click_link 'Add new' } + expect(all('#team_player_ids option', visible: false).select(&:selected?).map(&:value)).to match_array [players[0].id.to_s, player.id.to_s] + end + + it 'deselects the current selection' do + find('.ra-multiselect-selection option', text: /Elvis/).select_option + within('.ra-multiselect-center') { click_link 'Remove' } + expect(all('.ra-multiselect-selection option').map(&:text)).to be_empty + expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Cory Burns', 'Leonys Martin', 'Matt Garza', 'Elvis Andrus'] + expect(all('#team_player_ids option', visible: false).select(&:selected?).map(&:value)).to be_empty end + end + describe 'Choose all button' do it 'picks all available items' do visit edit_path(model_name: 'team', id: team.id) click_link 'Choose all' expect(all(:css, '#team_player_ids option', visible: false).map(&:value)).to match_array players.map(&:id).map(&:to_s) end + end - context 'when associated_collection_cache_all is false' do - before do - RailsAdmin.config Team do - field(:players) { associated_collection_cache_all false } - end + describe 'Clear all button' do + let!(:player) { FactoryBot.create :player, team: team, name: 'Elvis Andrus' } + + it 'removes all selected items' do + visit edit_path(model_name: 'team', id: team.id) + find('.ra-multiselect-collection option', text: /Cory/).select_option + within('.ra-multiselect-center') { click_link 'Add new' } + expect(all('.ra-multiselect-selection option').map(&:text)).to match_array ['Cory Burns', 'Elvis Andrus'] + click_link 'Clear all' + expect(all(:css, '#team_player_ids option', visible: false).select(&:selected?).map(&:value)).to be_empty + end + end + + context 'when using remote requests' do + before do + RailsAdmin.config Team do + field(:players) { associated_collection_cache_all false } end + visit edit_path(model_name: 'team', id: team.id) + end + + it 'supports filtering' do + find('input.ra-multiselect-search').set('Alex') + page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))") + is_expected.to have_content 'No objects found' + players[2].update name: 'Adam Rosales' + find('input.ra-multiselect-search').set('Ma') + page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))") + is_expected.to have_content 'Leonys' + expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Leonys Martin'] + end - it "does not pick the placeholder for selection" do - visit edit_path(model_name: 'team', id: team.id) + describe 'Choose all button' do + it 'does not pick the placeholder for selection' do click_link 'Choose all' expect(page).not_to have_css('#team_player_ids option', visible: false) expect(page).not_to have_css('.ra-multiselect-selection option') end it 'picks all available items' do - visit edit_path(model_name: 'team', id: team.id) - find('input.ra-multiselect-search').set('P') - page.execute_script("$('input.ra-multiselect-search').trigger('focus')") - page.execute_script("$('input.ra-multiselect-search').trigger('keydown')") - expect(page).to have_css('.ra-multiselect-collection option', text: /Player/) + find('input.ra-multiselect-search').set('Ma') + page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))") + expect(page).to have_css('.ra-multiselect-collection option', text: /Matt/) click_link 'Choose all' - expect(all(:css, '#team_player_ids option', visible: false).map(&:value)).to match_array players.map(&:id).map(&:to_s) + expect(all(:css, '#team_player_ids option', visible: false).map(&:value)).to match_array players[1..2].map(&:id).map(&:to_s) + end + end + end + + it 'does not cause duplication when using browser back' do + visit new_path(model_name: 'team') + find(%([href$="/admin/team/export"])).click + is_expected.to have_content 'Export Teams' + page.go_back + is_expected.to have_content 'New Team' + expect(all(:css, 'input.ra-multiselect-search').count).to eq 1 + end + + describe 'dynamic scoping' do + let!(:team) { FactoryBot.create :team, division: FactoryBot.create(:division) } + let(:division) { FactoryBot.create(:division) } + let!(:teams) { ['Los Angeles Dodgers', 'Texas Rangers'].map { |name| FactoryBot.create :team, name: name, division: division } } + before do + RailsAdmin.config Team do + field :name + field :division end + RailsAdmin.config Fan do + field :division, :enum do + enum { Division.pluck(:name, CI_ORM == :active_record ? :custom_id : :id).to_h } + def value + nil + end + + def parse_input(params) + params.delete :division + end + end + field :teams do + dynamically_scope_by :division + end + end + visit new_path(model_name: 'fan') + end + + it 'changes selection candidates based on value of the specified field' do + expect(all('#fan_team_ids option', visible: false).map(&:value).filter(&:present?)).to be_empty + select division.name, from: 'Division', visible: false + find('input.ra-multiselect-search').set('e') + page.execute_script("document.querySelector('input.ra-multiselect-search').dispatchEvent(new KeyboardEvent('keydown'))") + is_expected.to have_content 'Dodgers' + expect(all('.ra-multiselect-collection option').map(&:text)).to match_array ['Los Angeles Dodgers', 'Texas Rangers'] end end end diff --git a/spec/integration/widgets/filtering_select_spec.rb b/spec/integration/widgets/filtering_select_spec.rb new file mode 100644 index 0000000000..613df635af --- /dev/null +++ b/spec/integration/widgets/filtering_select_spec.rb @@ -0,0 +1,188 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Filtering select widget', type: :request, js: true do + subject { page } + + let!(:teams) { ['Los Angeles Dodgers', 'Texas Rangers'].map { |name| FactoryBot.create :team, name: name } } + let(:player) { FactoryBot.create :player, team: teams[0] } + before do + RailsAdmin.config Player do + field :team + field :number + end + end + + context 'on create' do + before { visit new_path(model_name: 'player') } + + it 'is initially unset' do + expect(find('input.ra-filtering-select-input').value).to be_empty + expect(find('#player_team_id', visible: false).value).to be_empty + end + + it 'supports filtering' do + find('input.ra-filtering-select-input').set('ge') + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") + is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') + expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to match_array ['Los Angeles Dodgers', 'Texas Rangers'] + find('input.ra-filtering-select-input').set('Los') + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") + is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') + expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to eq ['Los Angeles Dodgers'] + find('input.ra-filtering-select-input').set('Mets') + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") + is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') + expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to match_array ['No objects found'] + end + + it 'sets id of the selected item' do + find('input.ra-filtering-select-input').set('Tex') + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") + is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') + page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Texas Rangers")).click()} + expect(find('#player_team_id', visible: false).value).to eq teams[1].id.to_s + end + end + + context 'on update' do + it 'changes the selected value' do + visit edit_path(model_name: 'player', id: player.id) + expect(find('#player_team_id', visible: false).value).to eq teams[0].id.to_s + find('input.ra-filtering-select-input').set('Tex') + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") + is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') + page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Texas Rangers")).click()} + expect(find('#player_team_id', visible: false).value).to eq teams[1].id.to_s + end + + it 'clears the current selection with making the search box empty' do + visit edit_path(model_name: 'player', id: player.id) + find('input.ra-filtering-select-input').set('') + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keyup'))") + expect(find('#player_team_id', visible: false).value).to be_empty + end + + it 'clears the current selection with selecting the clear option' do + visit edit_path(model_name: 'player', id: player.id) + within('.filtering-select') { find('.dropdown-toggle').click } + find('a.ui-menu-item-wrapper', text: /Clear/).click + expect(find('#player_team_id', visible: false).value).to be_empty + end + + context 'when the field is required' do + before do + RailsAdmin.config Player do + field(:team) { required true } + end + visit edit_path(model_name: 'player', id: player.id) + end + + it 'does not show the clear option' do + within('.filtering-select') { find('.dropdown-toggle').click } + is_expected.not_to have_css('a.ui-menu-item-wrapper', text: /Clear/) + end + end + end + + it 'prevents duplication when using browser back and forward' do + player + visit index_path(model_name: 'player') + find(%([href$="/admin/player/#{player.id}/edit"])).click + is_expected.to have_content 'Edit Player' + page.go_back + is_expected.to have_content 'List of Players' + page.go_forward + is_expected.to have_content 'Edit Player' + expect(all(:css, 'input.ra-filtering-select-input').count).to eq 1 + end + + it 'does not lose options on browser back' do + visit edit_path(model_name: 'player', id: player.id) + find('.team_field .dropdown-toggle').click + find('li.ui-menu-item a', text: /Clear/).click + click_link 'Show' + is_expected.to have_content 'Details for Player' + page.go_back + find('.team_field input.ra-filtering-select-input').set('Los') + page.execute_script("document.querySelector('.team_field input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") + is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a', text: 'Los Angeles Dodgers') + end + + context 'when using remote requests' do + before do + RailsAdmin.config Player do + field :team do + associated_collection_cache_all false + end + end + visit new_path(model_name: 'player') + end + + it 'supports filtering' do + find('input.ra-filtering-select-input').set('ge') + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") + is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') + expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to match_array ['Los Angeles Dodgers', 'Texas Rangers'] + teams[0].update name: 'Cincinnati Reds' + find('input.ra-filtering-select-input').set('Red') + page.execute_script("document.querySelector('input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))") + is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') + expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to eq ['Cincinnati Reds'] + end + end + + describe 'dynamic scoping' do + let!(:players) { FactoryBot.create_list :player, 2, team: teams[1] } + let!(:freelancer) { FactoryBot.create :player, team: nil } + + context 'with single field' do + before do + player + RailsAdmin.config Draft do + field :team + field :player do + dynamically_scope_by :team + end + end + visit new_path(model_name: 'draft') + end + + it 'changes selection candidates based on value of the specified field' do + expect(all('#draft_player_id option', visible: false).map(&:value).filter(&:present?)).to be_empty + find('[data-input-for="draft_team_id"] input.ra-filtering-select-input').set('Tex') + page.execute_script(%{document.querySelector('[data-input-for="draft_team_id"] input.ra-filtering-select-input').dispatchEvent(new KeyboardEvent('keydown'))}) + is_expected.to have_selector('ul.ui-autocomplete li.ui-menu-item a') + page.execute_script %{[...document.querySelectorAll('ul.ui-autocomplete li.ui-menu-item')].find(e => e.innerText.includes("Texas Rangers")).click()} + within('[data-input-for="draft_player_id"].filtering-select') { find('.dropdown-toggle').click } + expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to match_array players.map(&:name) + end + + it 'allows filtering by blank value' do + within('[data-input-for="draft_player_id"].filtering-select') { find('.dropdown-toggle').click } + expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to match_array [freelancer.name] + end + end + + context 'with multiple fields' do + before do + player + RailsAdmin.config Draft do + field :team + field :player do + dynamically_scope_by [:team, {round: :number}] + end + field :round + end + visit new_path(model_name: 'draft', draft: {team_id: teams[1].id}) + end + + it 'changes selection candidates based on value of the specified fields' do + fill_in 'draft[round]', with: players[1].number + within('[data-input-for="draft_player_id"].filtering-select') { find('.dropdown-toggle').click } + expect(all(:css, 'ul.ui-autocomplete li.ui-menu-item a').map(&:text)).to match_array [players[1].name] + end + end + end +end diff --git a/spec/integration/widgets/nested_many_spec.rb b/spec/integration/widgets/nested_many_spec.rb new file mode 100644 index 0000000000..3121f39562 --- /dev/null +++ b/spec/integration/widgets/nested_many_spec.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Nested many widget', type: :request, js: true do + subject { page } + + let(:field_test) { FactoryBot.create :field_test } + let(:nested_field_tests) { %w[1 2].map { |i| NestedFieldTest.create! title: "title #{i}", field_test: field_test } } + before do + RailsAdmin.config(FieldTest) do + field :nested_field_tests + end + end + + it 'adds a new nested item' do + visit edit_path(model_name: 'field_test', id: field_test.id) + + find('#field_test_nested_field_tests_attributes_field .add_nested_fields').click + expect(page).to have_selector('.fields.tab-pane.active', visible: true) + + # trigger click via JS, workaround for instability in CI + execute_script %(document.querySelector('button[name="_save"]').click()) + is_expected.to have_content('Field test successfully updated') + + expect(field_test.nested_field_tests.length).to eq(1) + end + + it 'edits a nested item' do + nested_field_tests + visit edit_path(model_name: 'field_test', id: field_test.id) + + fill_in 'field_test_nested_field_tests_attributes_0_title', with: 'nested field test title 1 edited', visible: false + edited_id = find('#field_test_nested_field_tests_attributes_0_id', visible: false).value + + # trigger click via JS, workaround for instability in CI + execute_script %(document.querySelector('button[name="_save"]').click()) + is_expected.to have_content('Field test successfully updated') + + expect(field_test.nested_field_tests.find(edited_id).title).to eq('nested field test title 1 edited') + end + + it 'deletes a nested item' do + nested_field_tests + visit edit_path(model_name: 'field_test', id: field_test.id) + + find('#field_test_nested_field_tests_attributes_0__destroy', visible: false).set('true') + + # trigger click via JS, workaround for instability in CI + execute_script %(document.querySelector('button[name="_save"]').click()) + is_expected.to have_content('Field test successfully updated') + + expect(field_test.reload.nested_field_tests.map(&:id)).to eq [nested_field_tests[1].id] + end + + it 'sets bindings[:object] to nested object', js: false do + RailsAdmin.config(NestedFieldTest) do + nested do + field :title do + label do + bindings[:object].class.name + end + end + end + end + nested_field_tests + visit edit_path(model_name: 'field_test', id: field_test.id) + expect(find('#field_test_nested_field_tests_attributes_0_title_field')).to have_content('NestedFieldTest') + end + + it 'is deactivatable' do + visit new_path(model_name: 'field_test') + is_expected.to have_selector('#field_test_nested_field_tests_attributes_field .add_nested_fields') + RailsAdmin.config(FieldTest) do + configure :nested_field_tests do + nested_form false + end + end + visit new_path(model_name: 'field_test') + is_expected.to have_no_selector('#field_test_nested_field_tests_attributes_field .add_nested_fields') + end + + it 'is closable after adding a new item' do + visit new_path(model_name: 'field_test') + within('#field_test_nested_field_tests_attributes_field') do + find('.add_nested_fields').click + expect(page).to have_selector('.tab-content.collapse.show') + expect(page).to have_selector('.nav .nav-link.active', visible: true) + expect(page).to have_selector('.fields.tab-pane.active', visible: true) + find(':scope > .controls .toggler').click + expect(page).not_to have_selector('.tab-content.collapse.show') + expect(page).not_to have_selector('.nav .nav-link.active', visible: true) + expect(page).not_to have_selector('.fields.tab-pane.active', visible: true) + end + end + + it 'closes after removing all items' do + visit new_path(model_name: 'field_test') + within('#field_test_nested_field_tests_attributes_field') do + find('.add_nested_fields').click + expect(page).to have_selector('.tab-content.collapse.show') + find(':scope > .tab-content > .fields > .remove_nested_fields', visible: false).click + expect(page).not_to have_selector('.nav .nav-link.active', visible: true) + expect(page).not_to have_selector('.fields.tab-pane.active', visible: true) + end + end + + context 'with nested_attributes_options given' do + before do + allow(FieldTest.nested_attributes_options).to receive(:[]).with(any_args). + and_return(allow_destroy: true) + end + + it 'does not show destroy button except for newly created when :allow_destroy is false', js: false do + nested_field_tests + allow(FieldTest.nested_attributes_options).to receive(:[]).with(:nested_field_tests). + and_return(allow_destroy: false) + visit edit_path(model_name: 'field_test', id: field_test.id) + expect(find('#field_test_nested_field_tests_attributes_0_title').value).to eq('title 1') + is_expected.not_to have_selector('form .remove_nested_fields') + expect(find('div#nested_field_tests_fields_blueprint', visible: false)[:'data-blueprint']).to match( + /]* class="remove_nested_fields"[^>]*>/, + ) + end + end + + context "when a field which have the same name of nested_in field's" do + it "does not hide fields which are not associated with nesting parent field's model" do + visit new_path(model_name: 'field_test') + is_expected.not_to have_selector('select#field_test_nested_field_tests_attributes_new_nested_field_tests_field_test_id') + expect(find('div#nested_field_tests_fields_blueprint', visible: false)[:'data-blueprint']).to match( + /]* id="field_test_nested_field_tests_attributes_new_nested_field_tests_another_field_test_id"[^>]*>/, + ) + end + + it 'hides fields that are deeply nested with inverse_of' do + visit new_path(model_name: 'field_test') + expect(page.body).to_not include('field_test_nested_field_tests_attributes_new_nested_field_tests_deeply_nested_field_tests_attributes_new_deeply_nested_field_tests_nested_field_test_id_field') + expect(page.body).to include('field_test_nested_field_tests_attributes_new_nested_field_tests_deeply_nested_field_tests_attributes_new_deeply_nested_field_tests_title') + end + end + + context 'when XSS attack is attempted' do + it 'does not break on adding a new item' do + allow(I18n).to receive(:t).and_call_original + expect(I18n).to receive(:t).with('admin.form.new_model', name: 'Nested field test').and_return('') + visit edit_path(model_name: 'field_test', id: field_test.id) + find('#field_test_nested_field_tests_attributes_field .add_nested_fields').click + end + + it 'does not break on editing an existing item' do + NestedFieldTest.create! title: '', field_test: field_test + visit edit_path(model_name: 'field_test', id: field_test.id) + end + end +end diff --git a/spec/integration/widgets/nested_one_spec.rb b/spec/integration/widgets/nested_one_spec.rb new file mode 100644 index 0000000000..882f45097d --- /dev/null +++ b/spec/integration/widgets/nested_one_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Nested one widget', type: :request, js: true do + subject { page } + + let(:field_test) { FactoryBot.create :field_test } + before do + RailsAdmin.config(FieldTest) do + field :comment + end + end + + it 'adds an nested item' do + visit edit_path(model_name: 'field_test', id: field_test.id) + + find('#field_test_comment_attributes_field .add_nested_fields').click + fill_in 'field_test_comment_attributes_content', with: 'nested comment content' + + # trigger click via JS, workaround for instability in CI + execute_script %(document.querySelector('button[name="_save"]').click()) + is_expected.to have_content('Field test successfully updated') + + expect(field_test.reload.comment.content.strip).to eq('nested comment content') + end + + it 'deletes the nested item' do + FactoryBot.create :comment, commentable: field_test + visit edit_path(model_name: 'field_test', id: field_test.id) + + find('.comment_field .toggler').click + find('.comment_field .remove_nested_fields', visible: false).click + + # trigger click via JS, workaround for instability in CI + execute_script %(document.querySelector('button[name="_save"]').click()) + is_expected.to have_content('Field test successfully updated') + + expect(field_test.reload.comment).to be nil + end + + it 'is optional' do + visit edit_path(model_name: 'field_test', id: field_test.id) + click_button 'Save' + expect(field_test.reload.comment).to be_nil + end + + it 'is closable after adding a new item' do + visit new_path(model_name: 'field_test') + within('#field_test_comment_attributes_field') do + find('.add_nested_fields').click + expect(page).to have_selector('.tab-content.collapse.show') + expect(page).to have_selector('.fields.tab-pane.active', visible: true) + find(':scope > .controls .toggler').click + expect(page).not_to have_selector('.tab-content.collapse.show') + expect(page).not_to have_selector('.fields.tab-pane.active', visible: true) + end + end + + it 'closes after removing the item' do + visit new_path(model_name: 'field_test') + within('#field_test_comment_attributes_field') do + find('.add_nested_fields').click + expect(page).to have_selector('.tab-content.collapse.show') + find(':scope > .tab-content > .fields > .remove_nested_fields', visible: false).click + expect(page).not_to have_selector('.fields.tab-pane.active', visible: true) + end + end + + context 'when XSS attack is attempted' do + it 'does not break on adding a new item' do + allow(I18n).to receive(:t).and_call_original + expect(I18n).to receive(:t).with('admin.form.new_model', name: 'Comment').and_return('') + visit edit_path(model_name: 'field_test', id: field_test.id) + find('#field_test_comment_attributes_field .add_nested_fields').click + end + + it 'does not break on adding an existing item' do + RailsAdmin.config Comment do + object_label_method :content + end + FactoryBot.create :comment, content: '', commentable: field_test + visit edit_path(model_name: 'field_test', id: field_test.id) + end + end + + context 'when the nested field contains a required field' do + before do + RailsAdmin.config Comment do + configure :content do + required true + end + end + end + + it 'is not affected by form required validation' do + FactoryBot.create :comment, commentable: field_test, content: '' + visit edit_path(model_name: 'field_test', id: field_test.id) + + find('.comment_field .toggler').click + find('.comment_field .remove_nested_fields', visible: false).click + + # trigger click via JS, workaround for instability in CI + execute_script %(document.querySelector('button[name="_save"]').click()) + is_expected.to have_content('Field test successfully updated') + + expect(field_test.reload.comment).to be nil + end + end +end diff --git a/spec/integration/widgets/remote_form_spec.rb b/spec/integration/widgets/remote_form_spec.rb new file mode 100644 index 0000000000..ffcb1b5e6b --- /dev/null +++ b/spec/integration/widgets/remote_form_spec.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Remote form widget', type: :request, js: true do + subject { page } + + describe 'modal' do + it 'supports focusing on sub-modals' do + visit new_path(model_name: 'division') + click_link 'Add a new League' + is_expected.to have_content 'New League' + is_expected.not_to have_css '#modal.modal-static' + execute_script %($(document.body).append($(''))) + find('#sub-modal input').click + is_expected.to have_css '#sub-modal input:focus' + end + end + + context 'with filtering select widget' do + let(:league) { FactoryBot.create :league } + let(:division) { FactoryBot.create :division, league: league } + before do + RailsAdmin.config Division do + field :league + end + RailsAdmin.config League do + field :name + end + end + + it 'creates an associated record' do + visit new_path(model_name: 'division') + click_link 'Add a new League' + is_expected.to have_content 'New League' + fill_in 'Name', with: 'National League' + find('#modal .save-action').click + expect(find('#division_custom_league_id', visible: false).value).to eq League.first.id.to_s + expect(League.pluck(:name)).to eq ['National League'] + end + + it 'updates the associated record' do + visit edit_path(model_name: 'division', id: division.id) + expect(find('#division_custom_league_id', visible: false).value).to eq league.id.to_s + click_link 'Edit this League' + is_expected.to have_content "Edit League '#{league.name}'" + fill_in 'Name', with: 'National League' + find('#modal .save-action').click + expect(find('#division_custom_league_id', visible: false).value).to eq league.id.to_s + expect(league.reload.name).to eq 'National League' + end + end + + context 'with filtering multi-select widget' do + let(:leagues) { FactoryBot.create_list :league, 2 } + let!(:division) { FactoryBot.create :division, name: 'National League Central', league: leagues[0] } + before do + RailsAdmin.config League do + field :divisions + end + RailsAdmin.config Division do + field :name + field :league + end + end + + it 'creates an associated record and adds into selection' do + visit edit_path(model_name: 'league', id: leagues[1].id) + click_link 'Add a new Division' + is_expected.to have_content 'New Division' + fill_in 'Name', with: 'National League West' + find(%(#division_custom_league_id option[value="#{leagues[0].id}"]), visible: false).select_option + find('#modal .save-action').click + is_expected.to have_css('.ra-multiselect-selection option', text: 'National League West') + new_division = Division.where(name: 'National League West').first + expect(new_division).not_to be nil + expect(find('#league_division_ids', visible: false).value).to eq [new_division.id.to_s] + end + + it 'updates an unselected associated record with leaving it unselected' do + visit edit_path(model_name: 'league', id: leagues[1].id) + find('.ra-multiselect-collection option', text: division.name).double_click + is_expected.to have_content "Edit Division 'National League Central'" + fill_in 'Name', with: 'National League East' + find('#modal .save-action').click + is_expected.to have_css('.ra-multiselect-collection option', text: 'National League East') + expect(find('#league_division_ids', visible: false).value).to eq [] + expect(division.reload.name).to eq 'National League East' + end + + it 'updates a selected associated record' do + visit edit_path(model_name: 'league', id: leagues[0].id) + find('.ra-multiselect-selection option', text: division.name).double_click + is_expected.to have_content "Edit Division 'National League Central'" + fill_in 'Name', with: 'National League East' + find('#modal .save-action').click + expect(find('#league_division_ids', visible: false).value).to eq [division.id.to_s] + expect(division.reload.name).to eq 'National League East' + end + + context 'with inline_edit set to false' do + before do + RailsAdmin.config League do + field :divisions do + inline_edit false + end + end + end + + it 'does not open the modal with double click' do + visit edit_path(model_name: 'league', id: leagues[1].id) + find('.ra-multiselect-collection option', text: division.name).double_click + is_expected.not_to have_content "Edit Division 'National League Central'" + end + end + end + + context 'with file upload' do + before do + RailsAdmin.config NestedFieldTest do + field :field_test + end + RailsAdmin.config FieldTest do + field :carrierwave_asset + end + end + + it 'submits successfully' do + visit new_path(model_name: 'nested_field_test') + click_link 'Add a new Field test' + is_expected.to have_content 'New Field test' + attach_file 'Carrierwave asset', file_path('test.jpg') + find('#modal .save-action').click + is_expected.to have_css('option', text: /FieldTest #/, visible: false) + expect(FieldTest.first.carrierwave_asset.file.size).to eq 1575 + end + end + + context 'with validation errors' do + before do + RailsAdmin.config Team do + field :players + end + RailsAdmin.config Player do + field :name + field :number + field :team + end + end + + context 'on create' do + it 'shows the error messages' do + visit new_path(model_name: 'team') + click_link 'Add a new Player' + is_expected.to have_content 'New Player' + find('#player_name').set('on steroids') + find('#modal .save-action').click + is_expected.to have_css('#modal') + is_expected.to have_content 'Player is cheating' + is_expected.to have_css '.text-danger', text: 'is not a number' + end + end + + context 'on update' do + let!(:player) { FactoryBot.create :player, name: 'Cheater' } + + it 'shows the error messages' do + visit new_path(model_name: 'team') + find('option', text: 'Cheater').double_click + is_expected.to have_content "Edit Player 'Cheater'" + find('#player_name').set('Cheater on steroids') + find('#player_number').set('') + find('#modal .save-action').click + is_expected.to have_css('#modal') + is_expected.to have_content 'Player is cheating' + is_expected.to have_css '.text-danger', text: 'is not a number' + end + end + end +end diff --git a/spec/orm/active_record.rb b/spec/orm/active_record.rb index 5f6a49989b..b4f0717893 100644 --- a/spec/orm/active_record.rb +++ b/spec/orm/active_record.rb @@ -1,7 +1,6 @@ -require 'rails_admin/extensions/history/history' -require 'rails_admin/adapters/active_record' +# frozen_string_literal: true -DatabaseCleaner.strategy = :transaction +require 'rails_admin/adapters/active_record' ActiveRecord::Base.connection.data_sources.each do |table| ActiveRecord::Base.connection.drop_table(table) @@ -9,7 +8,7 @@ def silence_stream(stream) old_stream = stream.dup - stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null') + stream.reopen(/mswin|mingw/.match?(RbConfig::CONFIG['host_os']) ? 'NUL:' : '/dev/null') stream.sync = true yield ensure @@ -17,19 +16,19 @@ def silence_stream(stream) old_stream.close end -silence_stream(STDOUT) do +silence_stream($stdout) do if ActiveRecord::Migrator.respond_to? :migrate - ActiveRecord::Migrator.migrate File.expand_path('../../dummy_app/db/migrate/', __FILE__) + ActiveRecord::Migrator.migrate File.expand_path('../dummy_app/db/migrate', __dir__) else ActiveRecord::MigrationContext.new( - *([File.expand_path('../../dummy_app/db/migrate/', __FILE__)] + + *([File.expand_path('../dummy_app/db/migrate', __dir__)] + (ActiveRecord::MigrationContext.instance_method(:initialize).arity == 2 ? [ActiveRecord::SchemaMigration] : [])), ).migrate end end class Tableless < ActiveRecord::Base - class < value" do - it_behaves_like "filter on enum" do + context 'when enum is string enum where label <> value' do + it_behaves_like 'filter on enum' do let(:filter_value) { 's' } - let(:enum_field) { "string_enum_field" } + let(:enum_field) { 'string_enum_field' } let(:enum_label) { 'S' } let(:expected_elements_count) { 1 } end end end - context 'on dates with :en locale' do + context 'on dates' do before do [Date.new(2012, 1, 1), Date.new(2012, 1, 2), Date.new(2012, 1, 3), Date.new(2012, 1, 4)].each do |date| FactoryBot.create(:field_test, date_field: date) @@ -82,35 +106,29 @@ end it 'lists elements within outbound limits' do - expect(@abstract_model.all(filters: {'date_field' => {'1' => {v: ['', 'January 02, 2012', 'January 03, 2012'], o: 'between'}}}).count).to eq(2) - expect(@abstract_model.all(filters: {'date_field' => {'1' => {v: ['', 'January 02, 2012', 'January 02, 2012'], o: 'between'}}}).count).to eq(1) - expect(@abstract_model.all(filters: {'date_field' => {'1' => {v: ['', 'January 03, 2012', ''], o: 'between'}}}).count).to eq(2) - expect(@abstract_model.all(filters: {'date_field' => {'1' => {v: ['', '', 'January 02, 2012'], o: 'between'}}}).count).to eq(2) - expect(@abstract_model.all(filters: {'date_field' => {'1' => {v: ['January 02, 2012'], o: 'default'}}}).count).to eq(1) + expect(@abstract_model.all(filters: {'date_field' => {'1' => {v: ['', '2012-01-02', '2012-01-03'], o: 'between'}}}).count).to eq(2) + expect(@abstract_model.all(filters: {'date_field' => {'1' => {v: ['', '2012-01-02', '2012-01-02'], o: 'between'}}}).count).to eq(1) + expect(@abstract_model.all(filters: {'date_field' => {'1' => {v: ['', '2012-01-03', ''], o: 'between'}}}).count).to eq(2) + expect(@abstract_model.all(filters: {'date_field' => {'1' => {v: ['', '', '2012-01-02'], o: 'between'}}}).count).to eq(2) + expect(@abstract_model.all(filters: {'date_field' => {'1' => {v: ['2012-01-02'], o: 'default'}}}).count).to eq(1) end end - context 'on datetimes with :en locale' do + context 'on datetimes' do before do I18n.locale = :en FactoryBot.create(:field_test, datetime_field: Time.zone.local(2012, 1, 1, 23, 59, 59)) FactoryBot.create(:field_test, datetime_field: Time.zone.local(2012, 1, 2, 0, 0, 0)) FactoryBot.create(:field_test, datetime_field: Time.zone.local(2012, 1, 3, 23, 59, 59)) - - # TODO: Mongoid 3.0.0 mysteriously expands the range of inclusion slightly... - if defined?(Mongoid) && Mongoid::VERSION >= '3.0.0' - FactoryBot.create(:field_test, datetime_field: Time.zone.local(2012, 1, 4, 0, 0, 1)) - else - FactoryBot.create(:field_test, datetime_field: Time.zone.local(2012, 1, 4, 0, 0, 0)) - end + FactoryBot.create(:field_test, datetime_field: Time.zone.local(2012, 1, 4, 0, 0, 0)) end it 'lists elements within outbound limits' do - expect(@abstract_model.all(filters: {'datetime_field' => {'1' => {v: ['', 'January 02, 2012 12:00', 'January 03, 2012 12:00'], o: 'between'}}}).count).to eq(2) - expect(@abstract_model.all(filters: {'datetime_field' => {'1' => {v: ['', 'January 02, 2012 12:00', 'January 02, 2012 12:00'], o: 'between'}}}).count).to eq(1) - expect(@abstract_model.all(filters: {'datetime_field' => {'1' => {v: ['', 'January 03, 2012 12:00', ''], o: 'between'}}}).count).to eq(2) - expect(@abstract_model.all(filters: {'datetime_field' => {'1' => {v: ['', '', 'January 02, 2012 12:00'], o: 'between'}}}).count).to eq(2) - expect(@abstract_model.all(filters: {'datetime_field' => {'1' => {v: ['January 02, 2012 12:00'], o: 'default'}}}).count).to eq(1) + expect(@abstract_model.all(filters: {'datetime_field' => {'1' => {v: ['', '2012-01-02T00:00:00', '2012-01-03T23:59:59'], o: 'between'}}}).count).to eq(2) + expect(@abstract_model.all(filters: {'datetime_field' => {'1' => {v: ['', '2012-01-02T00:00:00', '2012-01-03T12:00:00'], o: 'between'}}}).count).to eq(1) + expect(@abstract_model.all(filters: {'datetime_field' => {'1' => {v: ['', '2012-01-03T12:00:00', ''], o: 'between'}}}).count).to eq(2) + expect(@abstract_model.all(filters: {'datetime_field' => {'1' => {v: ['', '', '2012-01-02T12:00:00'], o: 'between'}}}).count).to eq(2) + expect(@abstract_model.all(filters: {'datetime_field' => {'1' => {v: ['2012-01-02T00:00:00'], o: 'default'}}}).count).to eq(1) end end end diff --git a/spec/rails_admin/active_record_extension_spec.rb b/spec/rails_admin/active_record_extension_spec.rb index c91979c359..f9f9768e26 100644 --- a/spec/rails_admin/active_record_extension_spec.rb +++ b/spec/rails_admin/active_record_extension_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'spec_helper' -require File.expand_path('../../../config/initializers/active_record_extensions', __FILE__) +require File.expand_path('../../config/initializers/active_record_extensions', __dir__) RSpec.describe 'ActiveRecord::Base', active_record: true do describe '#safe_send' do diff --git a/spec/rails_admin/adapters/active_record/abstract_object_spec.rb b/spec/rails_admin/adapters/active_record/abstract_object_spec.rb deleted file mode 100644 index ebf9300769..0000000000 --- a/spec/rails_admin/adapters/active_record/abstract_object_spec.rb +++ /dev/null @@ -1,140 +0,0 @@ -require 'spec_helper' - -RSpec.describe 'RailsAdmin::Adapters::ActiveRecord::AbstractObject', active_record: true do - describe 'proxy' do - let(:object) { double('object') } - let(:abstract_object) { RailsAdmin::Adapters::ActiveRecord::AbstractObject.new(object) } - - it 'acts like a proxy' do - expect(object).to receive(:method_call) - abstract_object.method_call - end - - context 'when the method of underlying object receives keyword arguments' do - let(:object) { Class.new { def foo(bar: 1); end }.new } - - it 'does not break on proxying' do - expect { abstract_object.foo(bar: 2) }.not_to raise_error - end - end - end - - describe 'create' do - let(:player) { Player.new } - let(:object) { RailsAdmin::Adapters::ActiveRecord::AbstractObject.new player } - let(:name) { 'Stefan Kiszonka' } - let(:number) { 87 } - let(:position) { 'Fifth baseman' } - let(:suspended) { true } - - describe 'a record without associations' do - before do - object.set_attributes(name: name, number: number, position: position, suspended: suspended, team_id: nil) - end - - it 'creates a Player with given attributes' do - expect(object.save).to be_truthy - - player.reload - expect(player.name).to eq(name) - expect(player.number).to eq(number) - expect(player.position).to eq(position) - expect(player.suspended).to be_truthy - expect(player.draft).to be_nil - expect(player.team).to be_nil - end - end - - describe 'a record with has_one association' do - let(:draft) { FactoryBot.create(:draft) } - let(:number) { draft.player.number + 1 } # to avoid collision - - before do - object.set_attributes(name: name, number: number, position: position, suspended: suspended, team_id: nil, draft_id: draft.id) - end - - it 'creates a Player with given attributes' do - expect(object.save).to be_truthy - - player.reload - expect(player.name).to eq(name) - expect(player.number).to eq(number) - expect(player.position).to eq(position) - expect(player.suspended).to be_truthy - expect(player.draft).to eq(draft.reload) - expect(player.team).to be_nil - end - end - - describe 'a record with has_many associations' do - let(:league) { League.new } - let(:object) { RailsAdmin::Adapters::ActiveRecord::AbstractObject.new league } - let(:name) { 'Awesome League' } - let(:teams) { [FactoryBot.create(:team)] } - let(:divisions) { [Division.create!(name: 'div 1', league: League.create!(name: 'north')), Division.create!(name: 'div 2', league: League.create!(name: 'south'))] } - - before do - object.set_attributes(name: name, division_ids: divisions.collect(&:id)) - end - - it 'creates a League with given attributes and associations' do - expect(object.save).to be_truthy - league.reload - expect(league.name).to eq(name) - expect(league.divisions).to eq(divisions) - end - end - end - - describe 'update' do - describe 'a record with has_one association' do - let(:name) { 'Stefan Koza' } - let(:suspended) { true } - let(:player) { FactoryBot.create(:player, suspended: true, name: name, draft: FactoryBot.create(:draft)) } - let(:object) { RailsAdmin::Adapters::ActiveRecord::AbstractObject.new player } - let(:new_team) { FactoryBot.create(:team) } - let(:new_suspended) { false } - let(:new_draft) { nil } - let(:new_number) { player.number + 29 } - - before do - object.set_attributes(number: new_number, team_id: new_team.id, suspended: new_suspended, draft_id: new_draft) - object.save - end - - it 'updates a record and associations' do - object.reload - expect(object.number).to eq(new_number) - expect(object.name).to eq(name) - expect(object.draft).to be_nil - expect(object.suspended).to be_falsey - expect(object.team).to eq(new_team) - end - end - end - - describe 'destroy' do - let(:player) { FactoryBot.create(:player) } - let(:object) { RailsAdmin::Adapters::ActiveRecord::AbstractObject.new player } - - before do - object.destroy - end - - it 'deletes the record' do - expect(Player.exists?(player.id)).to be_falsey - end - end - - describe 'object_label_method' do - it 'is configurable' do - RailsAdmin.config League do - object_label_method { :custom_name } - end - - @league = FactoryBot.create :league - - expect(RailsAdmin.config('League').with(object: @league).object_label).to eq("League '#{@league.name}'") - end - end -end diff --git a/spec/rails_admin/adapters/active_record/association_spec.rb b/spec/rails_admin/adapters/active_record/association_spec.rb index f9ccf0c0d7..2d24126ca2 100644 --- a/spec/rails_admin/adapters/active_record/association_spec.rb +++ b/spec/rails_admin/adapters/active_record/association_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'timecop' @@ -18,7 +20,8 @@ class ARPost < Tableless end class ARCategory < Tableless - has_and_belongs_to_many :a_r_posts + has_and_belongs_to_many :a_r_posts, -> { readonly } + has_and_belongs_to_many :scoped_posts, ->(category) { where(id: category.id) }, class_name: 'ARPost' belongs_to :librarian, polymorphic: true end @@ -49,12 +52,12 @@ class ARComment < Tableless end it 'lists associations' do - expect(@post.associations.collect { |a| a.name.to_s }).to include(*%w(a_r_blog a_r_categories a_r_comments)) + expect(@post.associations.collect { |a| a.name.to_s }).to include(*%w[a_r_blog a_r_categories a_r_comments]) end it 'list associations types in supported [:belongs_to, :has_and_belongs_to_many, :has_many, :has_one]' do # ActiveRecord 4.1 converts has_and_belongs_to_many association to has_many - expect((@post.associations + @blog.associations + @user.associations).collect(&:type).uniq.collect(&:to_s)).to include(*%w(belongs_to has_many has_one)) + expect((@post.associations + @blog.associations + @user.associations).collect(&:type).uniq.collect(&:to_s)).to include(*%w[belongs_to has_many has_one]) end describe 'belongs_to association' do @@ -67,6 +70,7 @@ class ARComment < Tableless expect(subject.primary_key).to eq :id expect(subject.foreign_key).to eq :a_r_blog_id expect(subject.foreign_type).to be_nil + expect(subject.key_accessor).to eq :a_r_blog_id expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil @@ -85,6 +89,7 @@ class ARComment < Tableless expect(subject.primary_key).to eq :id expect(subject.foreign_key).to eq :ar_blog_id expect(subject.foreign_type).to be_nil + expect(subject.key_accessor).to eq :a_r_post_ids expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil @@ -162,16 +167,33 @@ class FieldTestWithSymbolForeignKey < FieldTest expect(subject.primary_key).to eq :id expect(subject.foreign_type).to be_nil expect(subject.foreign_key_nullable?).to be_truthy + expect(subject.key_accessor).to eq :a_r_category_ids expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil expect(subject.read_only?).to be_falsey expect(subject.nested_options).to be_nil end + + context 'with a scope given' do + subject { @category.associations.detect { |a| a.name == :a_r_posts } } + + it 'does not break' do + expect(subject.read_only?).to be_truthy + end + end + + context 'with a scope that receives an argument given' do + subject { @category.associations.detect { |a| a.name == :scoped_posts } } + + it 'ignores the scope' do + expect(subject.read_only?).to be_falsey + end + end end describe 'polymorphic belongs_to association' do - before { allow(RailsAdmin::Config).to receive(:models_pool).and_return(%w(ARBlog ARPost ARCategory ARUser ARProfile ARComment)) } + before { allow(RailsAdmin::Config).to receive(:models_pool).and_return(%w[ARBlog ARPost ARCategory ARUser ARProfile ARComment]) } subject { @comment.associations.detect { |a| a.name == :commentable } } it 'returns correct values' do @@ -181,6 +203,7 @@ class FieldTestWithSymbolForeignKey < FieldTest expect(subject.primary_key).to be_nil expect(subject.foreign_key).to eq :commentable_id expect(subject.foreign_type).to eq :commentable_type + expect(subject.key_accessor).to eq :commentable_id expect(subject.as).to be_nil expect(subject.polymorphic?).to be_truthy expect(subject.inverse_of).to be_nil @@ -192,6 +215,18 @@ class FieldTestWithSymbolForeignKey < FieldTest expect(@category.associations.detect { |a| a.name == :librarian }.klass).to eq [ARUser] expect(@blog.associations.detect { |a| a.name == :librarian }.klass).to eq [ARProfile] end + + describe 'on a subclass' do + before do + class ARReview < ARComment; end + allow(RailsAdmin::Config).to receive(:models_pool).and_return(%w[ARBlog ARPost ARCategory ARUser ARProfile ARComment ARReview]) + end + subject { RailsAdmin::AbstractModel.new(ARReview).associations.detect { |a| a.name == :commentable } } + + it 'returns correct target klasses' do + expect(subject.klass).to eq [ARBlog, ARPost] + end + end end describe 'polymorphic inverse has_many association' do @@ -204,6 +239,7 @@ class FieldTestWithSymbolForeignKey < FieldTest expect(subject.primary_key).to eq :id expect(subject.foreign_key).to eq :commentable_id expect(subject.foreign_type).to be_nil + expect(subject.key_accessor).to eq :a_r_comment_ids expect(subject.as).to eq :commentable expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil diff --git a/spec/rails_admin/adapters/active_record/object_extension_spec.rb b/spec/rails_admin/adapters/active_record/object_extension_spec.rb new file mode 100644 index 0000000000..a8d066f73c --- /dev/null +++ b/spec/rails_admin/adapters/active_record/object_extension_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'RailsAdmin::Adapters::ActiveRecord::ObjectExtension', active_record: true do + describe '#assign_attributes' do + let(:player) { Player.new } + let(:object) { player.extend RailsAdmin::Adapters::ActiveRecord::ObjectExtension } + + it 'does not cause error with nil' do + expect(object.assign_attributes(nil)).to be nil + end + end +end diff --git a/spec/rails_admin/adapters/active_record/property_spec.rb b/spec/rails_admin/adapters/active_record/property_spec.rb index 56021e1147..362bb9bffa 100644 --- a/spec/rails_admin/adapters/active_record/property_spec.rb +++ b/spec/rails_admin/adapters/active_record/property_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'timecop' @@ -24,4 +26,18 @@ expect(subject.serial?).to be_falsey end end + + describe '#read_only?' do + before do + class HasReadOnlyColumn < Tableless + column :name, :varchar + attr_readonly :name + end + end + + it 'returns correct values' do + expect(RailsAdmin::AbstractModel.new('Player').properties.detect { |f| f.name == :name }).not_to be_read_only + expect(RailsAdmin::AbstractModel.new('HasReadOnlyColumn').properties.detect { |f| f.name == :name }).to be_read_only + end + end end diff --git a/spec/rails_admin/adapters/active_record_spec.rb b/spec/rails_admin/adapters/active_record_spec.rb index 59227edc7f..444d870fd1 100644 --- a/spec/rails_admin/adapters/active_record_spec.rb +++ b/spec/rails_admin/adapters/active_record_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'timecop' @@ -10,15 +12,41 @@ end end let(:like) do - if ['postgresql', 'postgis'].include? activerecord_config[:adapter] + if %w[postgresql postgis].include? activerecord_config[:adapter] '(field ILIKE ?)' else '(LOWER(field) LIKE ?)' end end + let(:not_like) do + if %w[postgresql postgis].include? activerecord_config[:adapter] + '(field NOT ILIKE ?)' + else + '(LOWER(field) NOT LIKE ?)' + end + end def predicates_for(scope) scope.where_clause.instance_variable_get(:@predicates) + # .map do |predicate| + # if predicate.is_a? Arel::Nodes::BoundSqlLiteral + # binds = predicate.positional_binds + # predicate.sql_with_placeholders.delete_prefix('(').delete_suffix(')').gsub('?') do |_| + # bind = binds.shift + # case bind + # when Date + # "'#{bind.to_fs(:db)}'" + # when DateTime, Time + # "'#{bind.to_fs(:db)}'" + # else + # p bind + # bind.to_s + # end + # end + # else + # predicate + # end + # end end describe '#associations' do @@ -52,18 +80,22 @@ def predicates_for(scope) ] end - it '#new returns instance of AbstractObject' do - expect(abstract_model.new.object).to be_instance_of(Player) + it '#new returns an ActiveRecord instance' do + expect(abstract_model.new).to be_a(ActiveRecord::Base) end - it '#get returns instance of AbstractObject' do - expect(abstract_model.get(@players.first.id).object).to eq(@players.first) + it '#get returns an ActiveRecord instance' do + expect(abstract_model.get(@players.first.id)).to eq(@players.first) end it '#get returns nil when id does not exist' do expect(abstract_model.get('abc')).to be_nil end + it '#get returns an object that can be passed to ActiveJob' do + expect { NullJob.perform_later(abstract_model.get(@players.first.id)) }.not_to raise_error + end + it '#first returns a player' do expect(@players).to include abstract_model.first end @@ -90,7 +122,7 @@ class PlayerWithDefaultScope < Player it '#destroy destroys multiple items' do abstract_model.destroy(@players[0..1]) - expect(Player.all).to eq(@players[2..-1]) + expect(Player.all).to match_array(@players[2..]) end it '#where returns filtered results' do @@ -114,6 +146,12 @@ class PlayerWithDefaultScope < Player expect(abstract_model.all(bulk_ids: @players[0..1].collect(&:id))).to match_array @players[0..1] end + it 'supports retrieval by bulk_ids with composite primary keys', composite_primary_keys: true do + expect(RailsAdmin::AbstractModel.new(Fanship).all( + bulk_ids: %w[1_2 3_4], + ).to_sql.tr('`', '"')).to include 'WHERE ("fans_teams"."fan_id" = 1 AND "fans_teams"."team_id" = 2 OR "fans_teams"."fan_id" = 3 AND "fans_teams"."team_id" = 4)' + end + it 'supports pagination' do expect(abstract_model.all(sort: 'id', page: 2, per: 1)).to eq(@players[-2, 1]) expect(abstract_model.all(sort: 'id', page: 1, per: 2)).to eq(@players[-2, 2].reverse) @@ -121,6 +159,9 @@ class PlayerWithDefaultScope < Player it 'supports ordering' do expect(abstract_model.all(sort: 'id', sort_reverse: true)).to eq(@players.sort) + expect(abstract_model.all(sort: %w[id name], sort_reverse: true).to_sql.tr('`', '"')).to include('ORDER BY "players"."id" ASC, "players"."name" ASC') + expect(abstract_model.all(include: :team, sort: {players: :name, teams: :name}, sort_reverse: true).to_sql.tr('`', '"')).to include('ORDER BY "players"."name" ASC, "teams"."name" ASC') + expect { abstract_model.all(sort: 1, sort_reverse: true) }.to raise_error ArgumentError, /Unsupported/ end it 'supports querying' do @@ -270,13 +311,14 @@ def build_statement(type, value, operator) expect(build_statement(:string, 'foo', 'was')).to be_nil expect(build_statement(:string, 'foo', 'default')).to eq([like, '%foo%']) expect(build_statement(:string, 'foo', 'like')).to eq([like, '%foo%']) + expect(build_statement(:string, 'foo', 'not_like')).to eq([not_like, '%foo%']) expect(build_statement(:string, 'foo', 'starts_with')).to eq([like, 'foo%']) expect(build_statement(:string, 'foo', 'ends_with')).to eq([like, '%foo']) expect(build_statement(:string, 'foo', 'is')).to eq(['(field = ?)', 'foo']) end it 'performs case-insensitive searches' do - unless ['postgresql', 'postgis'].include?(activerecord_config[:adapter]) + unless %w[postgresql postgis].include?(activerecord_config[:adapter]) expect(build_statement(:string, 'foo', 'default')).to eq([like, '%foo%']) expect(build_statement(:string, 'FOO', 'default')).to eq([like, '%foo%']) end @@ -328,10 +370,10 @@ def build_statement(type, value, operator) describe 'boolean type queries' do it 'supports boolean type query' do - %w(false f 0).each do |value| + %w[false f 0].each do |value| expect(build_statement(:boolean, value, nil)).to eq(['(field IS NULL OR field = ?)', false]) end - %w(true t 1).each do |value| + %w[true t 1].each do |value| expect(build_statement(:boolean, value, nil)).to eq(['(field = ?)', true]) end expect(build_statement(:boolean, 'word', nil)).to be_nil @@ -339,13 +381,13 @@ def build_statement(type, value, operator) it "supports '_blank' operator" do [['_blank', ''], ['', '_blank']].each do |value, operator| - expect(build_statement(:boolean, value, operator)).to eq(["(field IS NULL)"]) + expect(build_statement(:boolean, value, operator)).to eq(['(field IS NULL)']) end end it "supports '_present' operator" do [['_present', ''], ['', '_present']].each do |value, operator| - expect(build_statement(:boolean, value, operator)).to eq(["(field IS NOT NULL)"]) + expect(build_statement(:boolean, value, operator)).to eq(['(field IS NOT NULL)']) end end @@ -363,13 +405,13 @@ def build_statement(type, value, operator) it "supports '_empty' operator" do [['_empty', ''], ['', '_empty']].each do |value, operator| - expect(build_statement(:boolean, value, operator)).to eq(["(field IS NULL)"]) + expect(build_statement(:boolean, value, operator)).to eq(['(field IS NULL)']) end end it "supports '_not_empty' operator" do [['_not_empty', ''], ['', '_not_empty']].each do |value, operator| - expect(build_statement(:boolean, value, operator)).to eq(["(field IS NOT NULL)"]) + expect(build_statement(:boolean, value, operator)).to eq(['(field IS NOT NULL)']) end end end @@ -385,7 +427,7 @@ def build_statement(type, value, operator) expect(build_statement(:integer, ['6', '', ''], 'default')).to eq(['(field = ?)', 6]) expect(build_statement(:integer, ['7', '10', ''], 'default')).to eq(['(field = ?)', 7]) expect(build_statement(:integer, ['8', '', '20'], 'default')).to eq(['(field = ?)', 8]) - expect(build_statement(:integer, %w(9 10 20), 'default')).to eq(['(field = ?)', 9]) + expect(build_statement(:integer, %w[9 10 20], 'default')).to eq(['(field = ?)', 9]) end it 'supports integer type range query' do @@ -394,7 +436,7 @@ def build_statement(type, value, operator) expect(build_statement(:integer, ['', '3', ''], 'between')).to eq(['(field >= ?)', 3]) expect(build_statement(:integer, ['', '', '5'], 'between')).to eq(['(field <= ?)', 5]) expect(build_statement(:integer, ['', '10', '20'], 'between')).to eq(['(field BETWEEN ? AND ?)', 10, 20]) - expect(build_statement(:integer, %w(15 10 20), 'between')).to eq(['(field BETWEEN ? AND ?)', 10, 20]) + expect(build_statement(:integer, %w[15 10 20], 'between')).to eq(['(field BETWEEN ? AND ?)', 10, 20]) expect(build_statement(:integer, ['', 'word1', ''], 'between')).to be_nil expect(build_statement(:integer, ['', '', 'word2'], 'between')).to be_nil expect(build_statement(:integer, ['', 'word3', 'word4'], 'between')).to be_nil @@ -445,9 +487,9 @@ def build_statement(type, value, operator) it "supports '_blank' operator" do [['_blank', ''], ['', '_blank']].each do |value, operator| aggregate_failures do - expect(build_statement(:integer, value, operator)).to eq(["(field IS NULL)"]) - expect(build_statement(:decimal, value, operator)).to eq(["(field IS NULL)"]) - expect(build_statement(:float, value, operator)).to eq(["(field IS NULL)"]) + expect(build_statement(:integer, value, operator)).to eq(['(field IS NULL)']) + expect(build_statement(:decimal, value, operator)).to eq(['(field IS NULL)']) + expect(build_statement(:float, value, operator)).to eq(['(field IS NULL)']) end end end @@ -455,9 +497,9 @@ def build_statement(type, value, operator) it "supports '_present' operator" do [['_present', ''], ['', '_present']].each do |value, operator| aggregate_failures do - expect(build_statement(:integer, value, operator)).to eq(["(field IS NOT NULL)"]) - expect(build_statement(:decimal, value, operator)).to eq(["(field IS NOT NULL)"]) - expect(build_statement(:float, value, operator)).to eq(["(field IS NOT NULL)"]) + expect(build_statement(:integer, value, operator)).to eq(['(field IS NOT NULL)']) + expect(build_statement(:decimal, value, operator)).to eq(['(field IS NOT NULL)']) + expect(build_statement(:float, value, operator)).to eq(['(field IS NOT NULL)']) end end end @@ -465,9 +507,9 @@ def build_statement(type, value, operator) it "supports '_null' operator" do [['_null', ''], ['', '_null']].each do |value, operator| aggregate_failures do - expect(build_statement(:integer, value, operator)).to eq(["(field IS NULL)"]) - expect(build_statement(:decimal, value, operator)).to eq(["(field IS NULL)"]) - expect(build_statement(:float, value, operator)).to eq(["(field IS NULL)"]) + expect(build_statement(:integer, value, operator)).to eq(['(field IS NULL)']) + expect(build_statement(:decimal, value, operator)).to eq(['(field IS NULL)']) + expect(build_statement(:float, value, operator)).to eq(['(field IS NULL)']) end end end @@ -475,9 +517,9 @@ def build_statement(type, value, operator) it "supports '_not_null' operator" do [['_not_null', ''], ['', '_not_null']].each do |value, operator| aggregate_failures do - expect(build_statement(:integer, value, operator)).to eq(["(field IS NOT NULL)"]) - expect(build_statement(:decimal, value, operator)).to eq(["(field IS NOT NULL)"]) - expect(build_statement(:float, value, operator)).to eq(["(field IS NOT NULL)"]) + expect(build_statement(:integer, value, operator)).to eq(['(field IS NOT NULL)']) + expect(build_statement(:decimal, value, operator)).to eq(['(field IS NOT NULL)']) + expect(build_statement(:float, value, operator)).to eq(['(field IS NOT NULL)']) end end end @@ -485,9 +527,9 @@ def build_statement(type, value, operator) it "supports '_empty' operator" do [['_empty', ''], ['', '_empty']].each do |value, operator| aggregate_failures do - expect(build_statement(:integer, value, operator)).to eq(["(field IS NULL)"]) - expect(build_statement(:decimal, value, operator)).to eq(["(field IS NULL)"]) - expect(build_statement(:float, value, operator)).to eq(["(field IS NULL)"]) + expect(build_statement(:integer, value, operator)).to eq(['(field IS NULL)']) + expect(build_statement(:decimal, value, operator)).to eq(['(field IS NULL)']) + expect(build_statement(:float, value, operator)).to eq(['(field IS NULL)']) end end end @@ -495,29 +537,45 @@ def build_statement(type, value, operator) it "supports '_not_empty' operator" do [['_not_empty', ''], ['', '_not_empty']].each do |value, operator| aggregate_failures do - expect(build_statement(:integer, value, operator)).to eq(["(field IS NOT NULL)"]) - expect(build_statement(:decimal, value, operator)).to eq(["(field IS NOT NULL)"]) - expect(build_statement(:float, value, operator)).to eq(["(field IS NOT NULL)"]) + expect(build_statement(:integer, value, operator)).to eq(['(field IS NOT NULL)']) + expect(build_statement(:decimal, value, operator)).to eq(['(field IS NOT NULL)']) + expect(build_statement(:float, value, operator)).to eq(['(field IS NOT NULL)']) end end end end - describe 'date type queries' do + describe 'date/time type queries' do let(:scope) { FieldTest.all } it 'supports date type query' do - expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: ['', 'February 01, 2012', 'March 01, 2012'], o: 'between'}}))).to eq(["(field_tests.date_field BETWEEN '2012-02-01' AND '2012-03-01')"]) - expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: ['', 'March 01, 2012', ''], o: 'between'}}))).to eq(["(field_tests.date_field >= '2012-03-01')"]) - expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: ['', '', 'February 01, 2012'], o: 'between'}}))).to eq(["(field_tests.date_field <= '2012-02-01')"]) - expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: ['February 01, 2012'], o: 'default'}}))).to eq(["(field_tests.date_field BETWEEN '2012-02-01' AND '2012-02-01')"]) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: ['', '2012-02-01', '2012-03-01'], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.date_field BETWEEN ? AND ?)', Date.new(2012, 2, 1), Date.new(2012, 3, 1)))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: ['', '2012-03-01', ''], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.date_field >= ?)', Date.new(2012, 3, 1)))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: ['', '', '2012-02-01'], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.date_field <= ?)', Date.new(2012, 2, 1)))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: ['2012-02-01'], o: 'default'}}))).to eq(predicates_for(scope.where('(field_tests.date_field = ?)', Date.new(2012, 2, 1)))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: [], o: 'today'}}))).to eq(predicates_for(scope.where('(field_tests.date_field = ?)', Date.today))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: [], o: 'yesterday'}}))).to eq(predicates_for(scope.where('(field_tests.date_field = ?)', Date.yesterday))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: [], o: 'this_week'}}))).to eq(predicates_for(scope.where('(field_tests.date_field BETWEEN ? AND ?)', Date.today.beginning_of_week, Date.today.end_of_week))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'date_field' => {'1' => {v: [], o: 'last_week'}}))).to eq(predicates_for(scope.where('(field_tests.date_field BETWEEN ? AND ?)', 1.week.ago.to_date.beginning_of_week, 1.week.ago.to_date.end_of_week))) end it 'supports datetime type query' do - expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: ['', 'February 01, 2012 12:00', 'March 01, 2012 12:00'], o: 'between'}}))).to eq(predicates_for(scope.where(['(field_tests.datetime_field BETWEEN ? AND ?)', Time.utc(2012, 2, 1), Time.utc(2012, 3, 1).end_of_day]))) - expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: ['', 'March 01, 2012 12:00', ''], o: 'between'}}))).to eq(predicates_for(scope.where(['(field_tests.datetime_field >= ?)', Time.utc(2012, 3, 1)]))) - expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: ['', '', 'February 01, 2012 12:00'], o: 'between'}}))).to eq(predicates_for(scope.where(['(field_tests.datetime_field <= ?)', Time.utc(2012, 2, 1).end_of_day]))) - expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: ['February 01, 2012 12:00'], o: 'default'}}))).to eq(predicates_for(scope.where(['(field_tests.datetime_field BETWEEN ? AND ?)', Time.utc(2012, 2, 1), Time.utc(2012, 2, 1).end_of_day]))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: ['', '2012-02-01T12:00:00', '2012-03-01T12:00:00'], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field BETWEEN ? AND ?)', Time.utc(2012, 2, 1, 12), Time.utc(2012, 3, 1, 12)))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: ['', '2012-03-01T12:00:00', ''], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field >= ?)', Time.utc(2012, 3, 1, 12)))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: ['', '', '2012-02-01T12:00:00'], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field <= ?)', Time.utc(2012, 2, 1, 12)))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: ['2012-02-01T12:00:00'], o: 'default'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field = ?)', Time.utc(2012, 2, 1, 12)))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: [], o: 'today'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field BETWEEN ? AND ?)', Date.today.beginning_of_day, Date.today.end_of_day))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: [], o: 'yesterday'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field BETWEEN ? AND ?)', Date.yesterday.beginning_of_day, Date.yesterday.end_of_day))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: [], o: 'this_week'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field BETWEEN ? AND ?)', Date.today.beginning_of_week.beginning_of_day, Date.today.end_of_week.end_of_day))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'datetime_field' => {'1' => {v: [], o: 'last_week'}}))).to eq(predicates_for(scope.where('(field_tests.datetime_field BETWEEN ? AND ?)', 1.week.ago.beginning_of_week, 1.week.ago.end_of_week))) + end + + it 'supports time type query' do + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'time_field' => {'1' => {v: ['', '2000-01-01T12:00:00', '2000-01-01T14:00:00'], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.time_field BETWEEN ? AND ?)', Time.utc(2000, 1, 1, 12), Time.utc(2000, 1, 1, 14)))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'time_field' => {'1' => {v: ['', '2000-01-01T14:00:00', ''], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.time_field >= ?)', Time.utc(2000, 1, 1, 14)))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'time_field' => {'1' => {v: ['', '', '2000-01-01T12:00:00'], o: 'between'}}))).to eq(predicates_for(scope.where('(field_tests.time_field <= ?)', Time.utc(2000, 1, 1, 12)))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'time_field' => {'1' => {v: ['2000-01-01T12:00:00'], o: 'default'}}))).to eq(predicates_for(scope.where('(field_tests.time_field = ?)', Time.utc(2000, 1, 1, 12)))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'time_field' => {'1' => {v: ['2021-02-03T12:00:00'], o: 'default'}}))).to eq(predicates_for(scope.where('(field_tests.time_field = ?)', Time.utc(2000, 1, 1, 12)))) end end @@ -529,17 +587,55 @@ def build_statement(type, value, operator) let(:scope) { FieldTest.all } it 'supports integer enum type query' do - expect(predicates_for(abstract_model.send(:filter_scope, scope, 'integer_enum_field' => {'1' => {v: 2, o: 'default'}}))).to eq(predicates_for(scope.where(['(field_tests.integer_enum_field IN (?))', 2]))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'integer_enum_field' => {'1' => {v: 2, o: 'default'}}))).to eq(predicates_for(scope.where('(field_tests.integer_enum_field IN (?))', [2]))) end it 'supports string enum type query' do - expect(predicates_for(abstract_model.send(:filter_scope, scope, 'string_enum_field' => {'1' => {v: 'm', o: 'default'}}))).to eq(predicates_for(scope.where(['(field_tests.string_enum_field IN (?))', 'm']))) + expect(predicates_for(abstract_model.send(:filter_scope, scope, 'string_enum_field' => {'1' => {v: 'm', o: 'default'}}))).to eq(predicates_for(scope.where('(field_tests.string_enum_field IN (?))', ['m']))) end end - it 'supports uuid type query' do - uuid = SecureRandom.uuid - expect(build_statement(:uuid, uuid, nil)).to eq(['(field = ?)', uuid]) + describe 'uuid type queries' do + it 'supports uuid type query' do + uuid = SecureRandom.uuid + expect(build_statement(:uuid, uuid, nil)).to eq(['(field = ?)', uuid]) + end + + it "supports '_blank' operator" do + [['_blank', ''], ['', '_blank']].each do |value, operator| + expect(build_statement(:uuid, value, operator)).to eq(['(field IS NULL)']) + end + end + + it "supports '_present' operator" do + [['_present', ''], ['', '_present']].each do |value, operator| + expect(build_statement(:uuid, value, operator)).to eq(['(field IS NOT NULL)']) + end + end + + it "supports '_null' operator" do + [['_null', ''], ['', '_null']].each do |value, operator| + expect(build_statement(:uuid, value, operator)).to eq(['(field IS NULL)']) + end + end + + it "supports '_not_null' operator" do + [['_not_null', ''], ['', '_not_null']].each do |value, operator| + expect(build_statement(:uuid, value, operator)).to eq(['(field IS NOT NULL)']) + end + end + + it "supports '_empty' operator" do + [['_empty', ''], ['', '_empty']].each do |value, operator| + expect(build_statement(:uuid, value, operator)).to eq(['(field IS NULL)']) + end + end + + it "supports '_not_empty' operator" do + [['_not_empty', ''], ['', '_not_empty']].each do |value, operator| + expect(build_statement(:uuid, value, operator)).to eq(['(field IS NOT NULL)']) + end + end end end diff --git a/spec/rails_admin/adapters/mongoid/abstract_object_spec.rb b/spec/rails_admin/adapters/mongoid/abstract_object_spec.rb deleted file mode 100644 index 40838dceb1..0000000000 --- a/spec/rails_admin/adapters/mongoid/abstract_object_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -require 'spec_helper' -require 'rails_admin/adapters/mongoid/abstract_object' - -RSpec.describe 'RailsAdmin::Adapters::Mongoid::AbstractObject', mongoid: true do - before do - @players = FactoryBot.create_list :player, 3 - @draft = FactoryBot.create :draft - @team = RailsAdmin::Adapters::Mongoid::AbstractObject.new FactoryBot.create :team - end - - describe 'references_many association' do - it 'supports retrieval of ids through foo_ids' do - expect(@team.player_ids).to eq([]) - player = FactoryBot.create :player, team: @team - expect(@team.player_ids).to eq([player.id]) - end - - it 'supports assignment of items through foo_ids=' do - expect(@team.players).to eq([]) - @team.player_ids = @players.collect(&:id) - @team.reload - expect(@team.players.collect(&:id)).to match_array @players.collect(&:id) - end - - it 'skips invalid id on assignment through foo_ids=' do - @team.player_ids = @players.collect { |item| item.id.to_s }.unshift('4f431021dcf2310db7000006') - @team.reload - @players.each(&:reload) - expect(@team.players.collect(&:id)).to match_array @players.collect(&:id) - end - - describe "model's custom setter" do - before do - Team.class_eval do - def player_ids=(players_ids) - self.custom_field = players_ids * ', ' - end - end - end - - after do - Team.class_eval do - undef player_ids= - end - end - - it 'have been called' do - expect(@team.custom_field).to eq(nil) - @team.player_ids = @players.collect(&:id) - expect(@team.custom_field).to eq(@players.collect(&:id) * ', ') - end - end - end - - describe 'references_one association' do - describe "model's custom setter" do - before do - Team.class_eval do - def draft_id=(draft_id) - self.custom_field = draft_id.to_s - end - end - end - - after do - Team.class_eval do - undef draft_id= - end - end - - it 'have been called' do - expect(@team.custom_field).to eq(nil) - @team.draft_id = @draft.id - expect(@team.custom_field).to eq(@draft.id.to_s) - end - end - end -end diff --git a/spec/rails_admin/adapters/mongoid/association_spec.rb b/spec/rails_admin/adapters/mongoid/association_spec.rb index c66c07fed1..94e8004f76 100644 --- a/spec/rails_admin/adapters/mongoid/association_spec.rb +++ b/spec/rails_admin/adapters/mongoid/association_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'RailsAdmin::Adapters::Mongoid::Association', mongoid: true do @@ -71,11 +73,11 @@ class MongoNote end it 'lists associations' do - expect(@post.associations.collect { |a| a.name.to_sym }).to match_array [:mongo_blog, :mongo_categories, :mongo_comments, :mongo_note] + expect(@post.associations.collect { |a| a.name.to_sym }).to match_array %i[mongo_blog mongo_categories mongo_comments mongo_note] end it 'reads correct and know types in [:belongs_to, :has_and_belongs_to_many, :has_many, :has_one]' do - expect((@post.associations + @blog.associations + @user.associations).collect { |a| a.type.to_s }.uniq).to match_array %w(belongs_to has_and_belongs_to_many has_many has_one) + expect((@post.associations + @blog.associations + @user.associations).collect { |a| a.type.to_s }.uniq).to match_array %w[belongs_to has_and_belongs_to_many has_many has_one] end describe 'belongs_to association' do @@ -90,6 +92,7 @@ class MongoNote expect(subject.foreign_key_nullable?).to be_truthy expect(subject.foreign_type).to be_nil expect(subject.foreign_inverse_of).to be_nil + expect(subject.key_accessor).to eq :mongo_blog_id expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil @@ -115,6 +118,7 @@ class MongoNote expect(subject.foreign_key_nullable?).to be_truthy expect(subject.foreign_type).to be_nil expect(subject.foreign_inverse_of).to be_nil + expect(subject.key_accessor).to eq :mongo_post_ids expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil @@ -135,6 +139,7 @@ class MongoNote expect(subject.foreign_key_nullable?).to be_truthy expect(subject.foreign_type).to be_nil expect(subject.foreign_inverse_of).to be_nil + expect(subject.key_accessor).to eq :mongo_category_ids expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil @@ -144,7 +149,7 @@ class MongoNote end describe 'polymorphic belongs_to association' do - before { allow(RailsAdmin::Config).to receive(:models_pool).and_return(%w(MongoBlog MongoPost MongoCategory MongoUser MongoProfile MongoComment)) } + before { allow(RailsAdmin::Config).to receive(:models_pool).and_return(%w[MongoBlog MongoPost MongoCategory MongoUser MongoProfile MongoComment]) } subject { @comment.associations.detect { |a| a.name == :commentable } } it 'returns correct values' do @@ -156,16 +161,29 @@ class MongoNote expect(subject.foreign_key_nullable?).to be_truthy expect(subject.foreign_type).to eq :commentable_type expect(subject.foreign_inverse_of).to be_nil + expect(subject.key_accessor).to eq :commentable_id expect(subject.as).to be_nil expect(subject.polymorphic?).to be_truthy expect(subject.inverse_of).to be_nil expect(subject.read_only?).to be_falsey expect(subject.nested_options).to be_nil end + + describe 'on a subclass' do + before do + class MongoReview < MongoComment; end + allow(RailsAdmin::Config).to receive(:models_pool).and_return(%w[MongoBlog MongoPost MongoCategory MongoUser MongoProfile MongoComment MongoReview]) + end + subject { RailsAdmin::AbstractModel.new(MongoReview).associations.detect { |a| a.name == :commentable } } + + it 'returns correct target klasses' do + expect(subject.klass).to eq [MongoBlog, MongoPost] + end + end end describe 'polymorphic inverse has_many association' do - before { allow(RailsAdmin::Config).to receive(:models_pool).and_return(%w(MongoBlog MongoPost MongoCategory MongoUser MongoProfile MongoComment)) } + before { allow(RailsAdmin::Config).to receive(:models_pool).and_return(%w[MongoBlog MongoPost MongoCategory MongoUser MongoProfile MongoComment]) } subject { @blog.associations.detect { |a| a.name == :mongo_comments } } it 'returns correct values' do @@ -177,6 +195,7 @@ class MongoNote expect(subject.foreign_key_nullable?).to be_truthy expect(subject.foreign_type).to be_nil expect(subject.foreign_inverse_of).to be_nil + expect(subject.key_accessor).to eq :mongo_comment_ids expect(subject.as).to eq :commentable expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil @@ -202,6 +221,7 @@ class MongoNote expect(subject.foreign_key_nullable?).to be_falsey expect(subject.foreign_type).to be_nil expect(subject.foreign_inverse_of).to be_nil + expect(subject.key_accessor).to be_nil expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil @@ -221,6 +241,7 @@ class MongoNote expect(subject.foreign_key).to be_nil expect(subject.foreign_key_nullable?).to be_falsey expect(subject.foreign_type).to be_nil + expect(subject.key_accessor).to be_nil expect(subject.as).to be_nil expect(subject.polymorphic?).to be_falsey expect(subject.inverse_of).to be_nil @@ -255,8 +276,8 @@ class MongoRecursivelyEmbedsMany recursively_embeds_many end - expect { RailsAdmin::AbstractModel.new(MongoEmbedsOne).associations.first.nested_options }.to raise_error(RuntimeError, "Embbeded association without accepts_nested_attributes_for can't be handled by RailsAdmin,\nbecause embedded model doesn't have top-level access.\nPlease add `accepts_nested_attributes_for :mongo_embedded' line to `MongoEmbedsOne' model.\n") - expect { RailsAdmin::AbstractModel.new(MongoEmbedsMany).associations.first.nested_options }.to raise_error(RuntimeError, "Embbeded association without accepts_nested_attributes_for can't be handled by RailsAdmin,\nbecause embedded model doesn't have top-level access.\nPlease add `accepts_nested_attributes_for :mongo_embeddeds' line to `MongoEmbedsMany' model.\n") + expect { RailsAdmin::AbstractModel.new(MongoEmbedsOne).associations.first.nested_options }.to raise_error(RuntimeError, "Embedded association without accepts_nested_attributes_for can't be handled by RailsAdmin,\nbecause embedded model doesn't have top-level access.\nPlease add `accepts_nested_attributes_for :mongo_embedded' line to `MongoEmbedsOne' model.\n") + expect { RailsAdmin::AbstractModel.new(MongoEmbedsMany).associations.first.nested_options }.to raise_error(RuntimeError, "Embedded association without accepts_nested_attributes_for can't be handled by RailsAdmin,\nbecause embedded model doesn't have top-level access.\nPlease add `accepts_nested_attributes_for :mongo_embeddeds' line to `MongoEmbedsMany' model.\n") expect { RailsAdmin::AbstractModel.new(MongoRecursivelyEmbedsOne).associations.first.nested_options }.not_to raise_error expect { RailsAdmin::AbstractModel.new(MongoRecursivelyEmbedsMany).associations.first.nested_options }.not_to raise_error end diff --git a/spec/rails_admin/adapters/mongoid/object_extension_spec.rb b/spec/rails_admin/adapters/mongoid/object_extension_spec.rb new file mode 100644 index 0000000000..20a5954d7a --- /dev/null +++ b/spec/rails_admin/adapters/mongoid/object_extension_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'RailsAdmin::Adapters::Mongoid::ObjectExtension', mongoid: true do + describe 'has_many association' do + let(:players) { FactoryBot.create_list :player, 2 } + before do + class TeamWithAutoSave < Team + has_many :players, inverse_of: :team, autosave: true + end + end + + context 'on create' do + before do + team.player_ids = players.collect(&:id) + team.players.each { |player| expect(player).to receive(:save).once.and_call_original } + team.save + end + + context 'with autosave: false' do + let(:team) { FactoryBot.build(:team).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } + + it 'persists associated documents changes on save' do + expect(team.reload.players).to match_array players + end + end + + context 'with autosave: true' do + let(:team) { TeamWithAutoSave.new(FactoryBot.attributes_for(:team)).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } + + it 'persists associated documents changes on save' do + expect(team.reload.players).to match_array players + end + end + end + + context 'on update' do + let(:team) { FactoryBot.create(:team).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } + before do + team.player_ids = players.collect(&:id) + end + + context 'with autosave: false' do + let(:team) { FactoryBot.create(:team).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } + + it 'persists associated documents changes on assignment' do + expect(team.reload.players).to match_array players + end + end + + context 'with autosave: true' do + let(:team) { TeamWithAutoSave.create(FactoryBot.attributes_for(:team)).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } + + it 'persists associated documents changes on assignment' do + expect(team.reload.players).to match_array players + end + end + end + end + + describe 'has_one association' do + let(:draft) { FactoryBot.create(:draft) } + before do + class PlayerWithAutoSave < Player + has_one :draft, inverse_of: :player, autosave: true + end + end + + context 'on create' do + before do + player.draft = draft + expect(player.draft._target).to receive(:save).once.and_call_original + player.save + end + + context 'with autosave: false' do + let(:player) { FactoryBot.build(:player).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } + + it 'persists associated documents changes on save' do + expect(player.reload.draft).to eq draft + end + end + + context 'with autosave: true' do + let(:player) { PlayerWithAutoSave.new(FactoryBot.attributes_for(:player)).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } + + it 'persists associated documents changes on save' do + expect(player.reload.draft).to eq draft + end + end + end + + context 'on update' do + before do + player.draft = draft + end + + context 'with autosave: false' do + let(:player) { FactoryBot.create(:player).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } + + it 'persists associated documents changes on assignment' do + expect(player.reload.draft).to eq draft + end + end + + context 'with autosave: true' do + let(:player) { PlayerWithAutoSave.create(FactoryBot.attributes_for(:player)).extend(RailsAdmin::Adapters::Mongoid::ObjectExtension) } + + it 'persists associated documents changes on assignment' do + expect(player.reload.draft).to eq draft + end + end + end + end +end diff --git a/spec/rails_admin/adapters/mongoid/property_spec.rb b/spec/rails_admin/adapters/mongoid/property_spec.rb index a34228cb14..2ff8c9336b 100644 --- a/spec/rails_admin/adapters/mongoid/property_spec.rb +++ b/spec/rails_admin/adapters/mongoid/property_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'RailsAdmin::Adapters::Mongoid::Property', mongoid: true do @@ -195,6 +197,15 @@ end end + describe 'aliased field' do + let(:field) { :aliased_field } + + it 'has correct values' do + expect(subject.name).to eq :aliased_field + expect(subject.pretty_name).to eq 'Aliased field' + end + end + describe '#length_validation_lookup' do it 'detects validation length properly' do class LengthValiated @@ -209,6 +220,7 @@ class LengthValiated class MyCustomValidator < ActiveModel::Validator def validate(_r); end end + class CustomValiated include Mongoid::Document field :text, type: String @@ -217,4 +229,19 @@ class CustomValiated expect { RailsAdmin::AbstractModel.new('CustomValiated').properties.last.send(:length_validation_lookup) }.not_to raise_error end end + + describe '#read_only?' do + before do + class HasReadOnlyColumn + include Mongoid::Document + field :name, type: String + attr_readonly :name + end + end + + it 'returns correct values' do + expect(RailsAdmin::AbstractModel.new('Player').properties.detect { |f| f.name == :name }).not_to be_read_only + expect(RailsAdmin::AbstractModel.new('HasReadOnlyColumn').properties.detect { |f| f.name == :name }).to be_read_only + end + end end diff --git a/spec/rails_admin/adapters/mongoid_spec.rb b/spec/rails_admin/adapters/mongoid_spec.rb index f9f9fe8518..fbd38114c6 100644 --- a/spec/rails_admin/adapters/mongoid_spec.rb +++ b/spec/rails_admin/adapters/mongoid_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'RailsAdmin::Adapters::Mongoid', mongoid: true do @@ -27,18 +29,26 @@ @abstract_model = RailsAdmin::AbstractModel.new('Player') end - it '#new returns instance of AbstractObject' do - expect(@abstract_model.new.object).to be_instance_of(Player) + it '#new returns a Mongoid::Document instance' do + expect(@abstract_model.new).to be_a(Mongoid::Document) end - it '#get returns instance of AbstractObject' do - expect(@abstract_model.get(@players.first.id.to_s).object).to eq(@players.first) + it '#get returns a Mongoid::Document instance' do + expect(@abstract_model.get(@players.first.id.to_s)).to eq(@players.first) end it '#get returns nil when id does not exist' do expect(@abstract_model.get('4f4f0824dcf2315093000000')).to be_nil end + context 'when Mongoid.raise_not_found_error is false' do + before { allow(Mongoid).to receive(:raise_not_found_error).and_return(false) } + + it '#get returns nil when id does not exist' do + expect(@abstract_model.get('4f4f0824dcf2315093000000')).to be_nil + end + end + it '#first returns a player' do expect(@players).to include @abstract_model.first end @@ -127,7 +137,7 @@ RailsAdmin.config Team do field :players do queryable true - searchable :all + searchable :name end end @teams = FactoryBot.create_list(:team, 3) @@ -151,7 +161,7 @@ RailsAdmin.config Team do field :fans do queryable true - searchable :all + searchable :name end end @teams = FactoryBot.create_list(:team, 3) @@ -304,10 +314,10 @@ def parse_value(value) end it 'supports boolean type query' do - %w(false f 0).each do |value| + %w[false f 0].each do |value| expect(@abstract_model.send(:build_statement, :field, :boolean, value, nil)).to eq(field: false) end - %w(true t 1).each do |value| + %w[true t 1].each do |value| expect(@abstract_model.send(:build_statement, :field, :boolean, value, nil)).to eq(field: true) end expect(@abstract_model.send(:build_statement, :field, :boolean, 'word', nil)).to be_nil @@ -324,7 +334,7 @@ def parse_value(value) expect(@abstract_model.send(:build_statement, :field, :integer, ['', '3', ''], 'between')).to eq(field: {'$gte' => 3}) expect(@abstract_model.send(:build_statement, :field, :integer, ['', '', '5'], 'between')).to eq(field: {'$lte' => 5}) expect(@abstract_model.send(:build_statement, :field, :integer, ['', '10', '20'], 'between')).to eq(field: {'$gte' => 10, '$lte' => 20}) - expect(@abstract_model.send(:build_statement, :field, :integer, %w(15 10 20), 'between')).to eq(field: {'$gte' => 10, '$lte' => 20}) + expect(@abstract_model.send(:build_statement, :field, :integer, %w[15 10 20], 'between')).to eq(field: {'$gte' => 10, '$lte' => 20}) expect(@abstract_model.send(:build_statement, :field, :integer, ['', 'word1', ''], 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :integer, ['', '', 'word2'], 'between')).to be_nil expect(@abstract_model.send(:build_statement, :field, :integer, ['', 'word3', 'word4'], 'between')).to be_nil @@ -377,23 +387,32 @@ def parse_value(value) expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'was')).to be_nil expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'default')).to eq(field: /foo/i) expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'like')).to eq(field: /foo/i) + expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'not_like')).to eq(field: /^((?!foo).)*$/i) expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'starts_with')).to eq(field: /^foo/i) expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'ends_with')).to eq(field: /foo$/i) expect(@abstract_model.send(:build_statement, :field, :string, 'foo', 'is')).to eq(field: 'foo') end it 'supports date type query' do - expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['', 'January 02, 2012', 'January 03, 2012'], o: 'between'}}).selector).to eq('$and' => [{'date_field' => {'$gte' => Date.new(2012, 1, 2), '$lte' => Date.new(2012, 1, 3)}}]) - expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['', 'January 03, 2012', ''], o: 'between'}}).selector).to eq('$and' => [{'date_field' => {'$gte' => Date.new(2012, 1, 3)}}]) - expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['', '', 'January 02, 2012'], o: 'between'}}).selector).to eq('$and' => [{'date_field' => {'$lte' => Date.new(2012, 1, 2)}}]) - expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['January 02, 2012'], o: 'default'}}).selector).to eq('$and' => [{'date_field' => {'$gte' => Date.new(2012, 1, 2), '$lte' => Date.new(2012, 1, 2)}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['', '2012-01-02', '2012-01-03'], o: 'between'}}).selector).to eq('$and' => [{'date_field' => {'$gte' => Date.new(2012, 1, 2), '$lte' => Date.new(2012, 1, 3)}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['', '2012-01-03', ''], o: 'between'}}).selector).to eq('$and' => [{'date_field' => {'$gte' => Date.new(2012, 1, 3)}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['', '', '2012-01-02'], o: 'between'}}).selector).to eq('$and' => [{'date_field' => {'$lte' => Date.new(2012, 1, 2)}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['2012-01-02'], o: 'default'}}).selector).to eq('$and' => [{'date_field' => Date.new(2012, 1, 2)}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: [], o: 'today'}}).selector).to eq('$and' => [{'date_field' => Date.today}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: [], o: 'yesterday'}}).selector).to eq('$and' => [{'date_field' => Date.yesterday}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: [], o: 'this_week'}}).selector).to eq('$and' => [{'date_field' => {'$gte' => Date.today.beginning_of_week, '$lte' => Date.today.end_of_week}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: [], o: 'last_week'}}).selector).to eq('$and' => [{'date_field' => {'$gte' => 1.week.ago.to_date.beginning_of_week, '$lte' => 1.week.ago.to_date.end_of_week}}]) end it 'supports datetime type query' do - expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['', 'January 02, 2012 00:00', 'January 03, 2012 00:00'], o: 'between'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Time.zone.local(2012, 1, 2), '$lte' => Time.zone.local(2012, 1, 3).end_of_day}}]) - expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['', 'January 03, 2012 00:00', ''], o: 'between'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Time.zone.local(2012, 1, 3)}}]) - expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['', '', 'January 02, 2012 00:00'], o: 'between'}}).selector).to eq('$and' => [{'datetime_field' => {'$lte' => Time.zone.local(2012, 1, 2).end_of_day}}]) - expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['January 02, 2012 00:00'], o: 'default'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Time.zone.local(2012, 1, 2), '$lte' => Time.zone.local(2012, 1, 2).end_of_day}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['', '2012-01-02T12:00:00', '2012-01-03T12:00:00'], o: 'between'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Time.zone.local(2012, 1, 2, 12), '$lte' => Time.zone.local(2012, 1, 3, 12)}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['', '2012-01-03T12:00:00', ''], o: 'between'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Time.zone.local(2012, 1, 3, 12)}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['', '', '2012-01-02T12:00:00'], o: 'between'}}).selector).to eq('$and' => [{'datetime_field' => {'$lte' => Time.zone.local(2012, 1, 2, 12)}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['2012-01-02T12:00:00'], o: 'default'}}).selector).to eq('$and' => [{'datetime_field' => Time.zone.local(2012, 1, 2, 12)}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: [], o: 'today'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Date.today.beginning_of_day, '$lte' => Date.today.end_of_day}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: [], o: 'yesterday'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Date.yesterday.beginning_of_day, '$lte' => Date.yesterday.end_of_day}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: [], o: 'this_week'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Date.today.beginning_of_week.beginning_of_day, '$lte' => Date.today.end_of_week.end_of_day}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: [], o: 'last_week'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => 1.week.ago.to_date.beginning_of_week.beginning_of_day, '$lte' => 1.week.ago.to_date.end_of_week.end_of_day}}]) end it 'supports enum type query' do diff --git a/spec/rails_admin/config/actions/base_spec.rb b/spec/rails_admin/config/actions/base_spec.rb index 8b6a5ac651..80d92ffd6e 100644 --- a/spec/rails_admin/config/actions/base_spec.rb +++ b/spec/rails_admin/config/actions/base_spec.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Actions::Base do - describe '#visible?' do + describe '#enabled?' do it 'excludes models not referenced in the only array' do RailsAdmin.config do |config| config.actions do @@ -10,9 +12,9 @@ end end end - expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Player))).to be_visible + expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Player))).to be_enabled expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Team))).to be_nil - expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Cms::BasicPage))).to be_visible + expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Cms::BasicPage))).to be_enabled end it 'excludes models referenced in the except array' do @@ -24,8 +26,40 @@ end end expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Player))).to be_nil - expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Team))).to be_visible + expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Team))).to be_enabled expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Cms::BasicPage))).to be_nil end + + it 'is always true for a writable model' do + RailsAdmin.config do |config| + config.actions do + index + show + new + edit + delete + end + end + %i[index show new edit delete].each do |action| + expect(RailsAdmin::Config::Actions.find(action, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(Player), object: Player.new)).to be_enabled + end + end + + it 'is false for write operations of a read-only model' do + RailsAdmin.config do |config| + config.actions do + index + show + new + edit + delete + end + end + expect(RailsAdmin::Config::Actions.find(:index, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(ReadOnlyComment))).to be_enabled + expect(RailsAdmin::Config::Actions.find(:show, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(ReadOnlyComment), object: ReadOnlyComment.new)).to be_enabled + expect(RailsAdmin::Config::Actions.find(:new, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(ReadOnlyComment))).to be_enabled + expect(RailsAdmin::Config::Actions.find(:edit, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(ReadOnlyComment), object: ReadOnlyComment.new)).to be_nil + expect(RailsAdmin::Config::Actions.find(:delete, controller: double(authorized?: true), abstract_model: RailsAdmin::AbstractModel.new(ReadOnlyComment), object: ReadOnlyComment.new)).to be_nil + end end end diff --git a/spec/rails_admin/config/actions_spec.rb b/spec/rails_admin/config/actions_spec.rb index 23ca540105..ffc6ee6bc7 100644 --- a/spec/rails_admin/config/actions_spec.rb +++ b/spec/rails_admin/config/actions_spec.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Actions do describe 'default' do it 'is as before' do - expect(RailsAdmin::Config::Actions.all.collect(&:key)).to eq([:dashboard, :index, :show, :new, :edit, :export, :delete, :bulk_delete, :history_show, :history_index, :show_in_app]) + expect(RailsAdmin::Config::Actions.all.collect(&:key)).to eq(%i[dashboard index show new edit export delete bulk_delete history_show history_index show_in_app]) end end @@ -72,7 +74,7 @@ end end - expect(RailsAdmin::Config::Actions.all.collect(&:key)).to eq([:dashboard, :index]) + expect(RailsAdmin::Config::Actions.all.collect(&:key)).to eq(%i[dashboard index]) end it 'restricts by scope' do @@ -114,7 +116,7 @@ end end - expect(RailsAdmin::Config::Actions.all.collect(&:key)).to eq([:dashboard, :index, :show]) + expect(RailsAdmin::Config::Actions.all.collect(&:key)).to eq(%i[dashboard index show]) end it 'allows to customize the custom_key when customizing an existing action' do @@ -172,8 +174,8 @@ end end - expect(RailsAdmin::Config::Actions.all.collect(&:custom_key)).to eq([:dashboard, :my_dashboard]) - expect(RailsAdmin::Config::Actions.all.collect(&:key)).to eq([:dashboard, :dashboard]) + expect(RailsAdmin::Config::Actions.all.collect(&:custom_key)).to eq(%i[dashboard my_dashboard]) + expect(RailsAdmin::Config::Actions.all.collect(&:key)).to eq(%i[dashboard dashboard]) end end end diff --git a/spec/rails_admin/config/configurable_spec.rb b/spec/rails_admin/config/configurable_spec.rb index a8f343b89c..4abfc83358 100644 --- a/spec/rails_admin/config/configurable_spec.rb +++ b/spec/rails_admin/config/configurable_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Configurable do diff --git a/spec/rails_admin/config/const_load_suppressor_spec.rb b/spec/rails_admin/config/const_load_suppressor_spec.rb new file mode 100644 index 0000000000..4da84d1403 --- /dev/null +++ b/spec/rails_admin/config/const_load_suppressor_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RailsAdmin::Config::ConstLoadSuppressor do + describe '.suppressing' do + it 'suppresses constant loading' do + expect do + subject.suppressing { UnknownConstant } + end.not_to raise_error + end + + it 'raises the error on recursion' do + expect do + subject.suppressing do + subject.suppressing {} + end + end.to raise_error(/already suppressed/) + end + end + + describe '.allowing' do + it 'suspends constant loading suppression' do + expect do + subject.suppressing do + subject.allowing { UnknownConstant } + end + end.to raise_error NameError + end + + it 'does not break when suppression is disabled' do + expect do + subject.allowing {} + end.not_to raise_error + end + end +end diff --git a/spec/rails_admin/config/fields/association_spec.rb b/spec/rails_admin/config/fields/association_spec.rb index 7c3c660c6e..bc6cf624f3 100644 --- a/spec/rails_admin/config/fields/association_spec.rb +++ b/spec/rails_admin/config/fields/association_spec.rb @@ -1,18 +1,116 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Association do describe '#pretty_value' do let(:player) { FactoryBot.create(:player, name: '
    ', team: FactoryBot.create(:team)) } let(:field) { RailsAdmin.config('Team').fields.detect { |f| f.name == :players } } - let(:view) { (ActionView.version.to_s >= '6' ? ActionView::Base.empty : ActionView::Base.new).tap { |d| allow(d).to receive(:action).and_return(nil) } } + let(:view) { ActionView::Base.empty } subject { field.with(object: player.team, view: view).pretty_value } context 'when the link is disabled' do + let(:view) { ActionView::Base.empty.tap { |d| allow(d).to receive(:action).and_return(nil) } } + it 'does not expose non-HTML-escaped string' do is_expected.to be_html_safe is_expected.to eq '<br />' end end + + context 'when the value is empty' do + let(:team) { FactoryBot.build :team } + subject { field.with(object: team, view: view).pretty_value } + + it "returns '-' to show emptiness" do + is_expected.to eq '-' + end + end + end + + describe '#dynamic_scope_relationships' do + let(:player) { FactoryBot.create(:player, team: FactoryBot.create(:team)) } + let(:field) { RailsAdmin.config('Draft').fields.detect { |f| f.name == :player } } + + it 'returns the relationship of fields in this model and in the associated model' do + RailsAdmin.config Draft do + field :team + field :player do + dynamically_scope_by :team + end + end + expect(field.dynamic_scope_relationships).to eq({team_id: :team}) + end + + it 'accepts Array' do + RailsAdmin.config Draft do + field :team + field :notes + field :player do + dynamically_scope_by %i[team notes] + end + end + expect(field.dynamic_scope_relationships).to eq({team_id: :team, notes: :notes}) + end + + it 'accepts Hash' do + RailsAdmin.config Draft do + field :round + field :player do + dynamically_scope_by({round: :number}) + end + end + expect(field.dynamic_scope_relationships).to eq({round: :number}) + end + + it 'accepts mixture of Array and Hash' do + RailsAdmin.config Draft do + field :team + field :round + field :player do + dynamically_scope_by [:team, {round: :number}] + end + end + expect(field.dynamic_scope_relationships).to eq({team_id: :team, round: :number}) + end + + it 'raises error if the field does not exist in this model' do + RailsAdmin.config Draft do + field :player do + dynamically_scope_by :team + end + end + expect { field.dynamic_scope_relationships }.to raise_error "Field 'team' was given for #dynamically_scope_by but not found in 'Draft'" + end + + it 'raises error if the field does not exist in the associated model' do + RailsAdmin.config Player do + field :name + end + RailsAdmin.config Draft do + field :team + field :player do + dynamically_scope_by :team + end + end + expect { field.dynamic_scope_relationships }.to raise_error "Field 'team' was given for #dynamically_scope_by but not found in 'Player'" + end + + it 'raises error if the target field is not filterable' do + RailsAdmin.config Player do + field :name + field :team do + filterable false + end + end + RailsAdmin.config Draft do + field :team + field :player do + dynamically_scope_by :team + end + end + expect { field.dynamic_scope_relationships }.to raise_error "Field 'team' in 'Player' can't be used for dynamic scoping because it's not filterable" + end end describe '#removable?', active_record: true do @@ -50,4 +148,24 @@ class TeamWithHasManyThrough < Team end end end + + describe '#value' do + context 'when using `system` as the association name' do + before do + class System < Tableless; end + + class BelongsToSystem < Tableless + column :system_id, :integer + belongs_to :system + end + end + let(:record) { BelongsToSystem.new(system: System.new) } + let(:field) { RailsAdmin.config(BelongsToSystem).fields.detect { |f| f.name == :system } } + subject { field.with(object: record).value } + + it 'does not break' do + expect(subject).to be_a_kind_of System + end + end + end end diff --git a/spec/rails_admin/config/fields/base_spec.rb b/spec/rails_admin/config/fields/base_spec.rb index 363222b299..195b17d03e 100644 --- a/spec/rails_admin/config/fields/base_spec.rb +++ b/spec/rails_admin/config/fields/base_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Base do @@ -61,8 +63,8 @@ class ConditionalValidationTest < Tableless column :foo, :varchar column :bar, :varchar - validates :foo, presence: true, if: :presisted? - validates :bar, presence: true, unless: :presisted? + validates :foo, presence: true, if: :persisted? + validates :bar, presence: true, unless: :persisted? end end @@ -122,7 +124,7 @@ class RelTest < Tableless end describe '#children_fields' do - POLYMORPHIC_CHILDREN = [:commentable_id, :commentable_type].freeze + POLYMORPHIC_CHILDREN = %i[commentable_id commentable_type].freeze it 'is empty by default' do expect(RailsAdmin.config(Team).fields.detect { |f| f.name == :name }.children_fields).to eq([]) @@ -159,7 +161,7 @@ class CommentReversed < Tableless context 'of a Dragonfly installation' do it 'is a _name field and _uid field' do - expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :dragonfly_asset }.children_fields).to eq([:dragonfly_asset_name, :dragonfly_asset_uid]) + expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :dragonfly_asset }.children_fields).to eq(%i[dragonfly_asset_name dragonfly_asset_uid]) end end @@ -180,11 +182,11 @@ class CommentReversed < Tableless if defined?(ActiveStorage) context 'of a ActiveStorage installation' do it 'is _attachment and _blob fields' do - expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :active_storage_asset }.children_fields).to match_array [:active_storage_asset_attachment, :active_storage_asset_blob] + expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :active_storage_asset }.children_fields).to match_array %i[active_storage_asset_attachment active_storage_asset_blob] end it 'is hidden, not filterable' do - fields = RailsAdmin.config(FieldTest).fields.select { |f| [:active_storage_asset_attachment, :active_storage_asset_blob].include?(f.name) } + fields = RailsAdmin.config(FieldTest).fields.select { |f| %i[active_storage_asset_attachment active_storage_asset_blob].include?(f.name) } expect(fields).to all(be_hidden) expect(fields).not_to include(be_filterable) end @@ -192,11 +194,11 @@ class CommentReversed < Tableless context 'of a ActiveStorage installation with multiple file support' do it 'is _attachment and _blob fields' do - expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :active_storage_assets }.children_fields).to match_array [:active_storage_assets_attachments, :active_storage_assets_blobs] + expect(RailsAdmin.config(FieldTest).fields.detect { |f| f.name == :active_storage_assets }.children_fields).to match_array %i[active_storage_assets_attachments active_storage_assets_blobs] end it 'is hidden, not filterable' do - fields = RailsAdmin.config(FieldTest).fields.select { |f| [:active_storage_assets_attachments, :active_storage_assets_blobs].include?(f.name) } + fields = RailsAdmin.config(FieldTest).fields.select { |f| %i[active_storage_assets_attachments active_storage_assets_blobs].include?(f.name) } expect(fields).to all(be_hidden) expect(fields).not_to include(be_filterable) end @@ -518,12 +520,6 @@ class CommentReversed < Tableless end end - describe '#associated_collection' do - it 'returns [] when type is blank?' do - expect(RailsAdmin.config(Comment).fields.detect { |f| f.name == :commentable }.associated_collection('')).to be_empty - end - end - describe '#visible?' do it 'is false when fields have specific name ' do class FieldVisibilityTest < Tableless @@ -538,8 +534,8 @@ class FieldVisibilityTest < Tableless column :updated_on, :timestamp column :deleted_on, :timestamp end - expect(RailsAdmin.config(FieldVisibilityTest).base.fields.select(&:visible?).collect(&:name)).to match_array [:_id, :created_at, :created_on, :deleted_at, :deleted_on, :id, :name, :updated_at, :updated_on] - expect(RailsAdmin.config(FieldVisibilityTest).list.fields.select(&:visible?).collect(&:name)).to match_array [:_id, :created_at, :created_on, :deleted_at, :deleted_on, :id, :name, :updated_at, :updated_on] + expect(RailsAdmin.config(FieldVisibilityTest).base.fields.select(&:visible?).collect(&:name)).to match_array %i[_id created_at created_on deleted_at deleted_on id name updated_at updated_on] + expect(RailsAdmin.config(FieldVisibilityTest).list.fields.select(&:visible?).collect(&:name)).to match_array %i[_id created_at created_on deleted_at deleted_on id name updated_at updated_on] expect(RailsAdmin.config(FieldVisibilityTest).edit.fields.select(&:visible?).collect(&:name)).to match_array [:name] expect(RailsAdmin.config(FieldVisibilityTest).show.fields.select(&:visible?).collect(&:name)).to match_array [:name] end @@ -573,4 +569,35 @@ class FieldVisibilityTest < Tableless expect(division_field.default_filter_operator).to be nil # rails_admin generic fallback end end + + describe '#eager_load' do + let(:field) { RailsAdmin.config('Team').fields.detect { |f| f.name == :players } } + + it 'can be set to true' do + RailsAdmin.config Team do + field :players do + eager_load true + end + end + expect(field.eager_load_values).to eq [:players] + end + + it 'can be set to false' do + RailsAdmin.config Team do + field :players do + eager_load false + end + end + expect(field.eager_load_values).to eq [] + end + + it 'can be set to a custom value' do + RailsAdmin.config Team do + field :players do + eager_load [{players: :draft}, :fans] + end + end + expect(field.eager_load_values).to eq [{players: :draft}, :fans] + end + end end diff --git a/spec/rails_admin/config/fields/types/action_text_spec.rb b/spec/rails_admin/config/fields/types/action_text_spec.rb index 89f48dce66..6c78fe8ca3 100644 --- a/spec/rails_admin/config/fields/types/action_text_spec.rb +++ b/spec/rails_admin/config/fields/types/action_text_spec.rb @@ -1,7 +1,11 @@ +# frozen_string_literal: true + require 'spec_helper' -RSpec.describe RailsAdmin::Config::Fields::Types::ActionText do - it_behaves_like 'a generic field type', :action_text_field +if defined?(ActionText) + RSpec.describe RailsAdmin::Config::Fields::Types::ActionText do + it_behaves_like 'a generic field type', :action_text_field - it_behaves_like 'a string-like field type', :action_text_field -end if defined?(ActionText) + it_behaves_like 'a string-like field type', :action_text_field + end +end diff --git a/spec/rails_admin/config/fields/types/active_record_enum_spec.rb b/spec/rails_admin/config/fields/types/active_record_enum_spec.rb index 988c658d8e..2661215d44 100644 --- a/spec/rails_admin/config/fields/types/active_record_enum_spec.rb +++ b/spec/rails_admin/config/fields/types/active_record_enum_spec.rb @@ -1,5 +1,30 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::ActiveRecordEnum, active_record: true do it_behaves_like 'a generic field type', :string_enum_field + + describe '#pretty_value' do + context 'when column name is format' do + before do + class FormatAsEnum < FieldTest + if ActiveRecord.gem_version >= Gem::Version.new('7.0') + enum :format, {Text: 'txt', Markdown: 'md'} + else + enum format: {Text: 'txt', Markdown: 'md'} + end + end + end + let(:field) do + RailsAdmin.config(FormatAsEnum).fields.detect do |f| + f.name == :format + end.with(object: FormatAsEnum.new(format: 'md')) + end + + it 'does not break' do + expect(field.pretty_value).to eq 'Markdown' + end + end + end end diff --git a/spec/rails_admin/config/fields/types/active_storage_spec.rb b/spec/rails_admin/config/fields/types/active_storage_spec.rb index 9e60eb8328..01a8dacaae 100644 --- a/spec/rails_admin/config/fields/types/active_storage_spec.rb +++ b/spec/rails_admin/config/fields/types/active_storage_spec.rb @@ -1,84 +1,157 @@ +# frozen_string_literal: true + require 'spec_helper' -RSpec.describe RailsAdmin::Config::Fields::Types::ActiveStorage do - it_behaves_like 'a generic field type', :string_field, :active_storage +if defined?(ActiveStorage) + RSpec.describe RailsAdmin::Config::Fields::Types::ActiveStorage do + it_behaves_like 'a generic field type', :string_field, :active_storage - let(:record) { FactoryBot.create :field_test } - let(:field) do - RailsAdmin.config('FieldTest').fields.detect do |f| - f.name == :active_storage_asset - end.with(object: record) - end + let(:record) { FactoryBot.create :field_test } + let(:field) do + RailsAdmin.config('FieldTest').fields.detect do |f| + f.name == :active_storage_asset + end.with(object: record) + end - describe '#thumb_method' do - it 'returns corresponding value which is to be passed to image_processing(ActiveStorage >= 6.0) or mini_magick(ActiveStorage 5.2)' do - if ::ActiveStorage::VERSION::MAJOR >= 6 + describe '#thumb_method' do + it 'returns corresponding value which is to be passed to image_processing(ActiveStorage >= 6.0) or mini_magick(ActiveStorage 5.2)' do expect(field.thumb_method).to eq(resize_to_limit: [100, 100]) - else - expect(field.thumb_method).to eq(resize: '100x100>') end end - end - describe '#image?' do - context 'when attachment is an image' do - let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: "test.jpg", content_type: "image/jpeg"} } + describe '#image?' do + context 'configured Mime::Types' do + before { Mime::Type.register 'image/webp', :webp } + after { Mime::Type.unregister :webp } - it 'returns true' do - expect(field.image?).to be_truthy + %w[jpg jpeg png gif svg webp].each do |image_type_ext| + context "when attachment is a '#{image_type_ext}' file" do + let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: "test.#{image_type_ext}"} } + + it 'returns true' do + expect(field.image?).to be_truthy + end + end + end end - end - context 'when attachment is not an image' do - let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: "test.txt", content_type: "text/plain"} } + context 'when attachment is not an image' do + let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: 'test.txt', content_type: 'text/plain'} } - it 'returns false' do - expect(field.image?).to be_falsy + it 'returns false' do + expect(field.image?).to be_falsy + end end - end - end - describe '#resource_url' do - context 'when calling with thumb = false' do - let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: "test.jpg", content_type: "image/jpeg"} } + context 'when attachment is a PDF file' do + let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: 'test.pdf', content_type: 'application/pdf'} } + before { allow(ActiveStorage::Previewer::PopplerPDFPreviewer).to receive(:accept?).and_return(true) } - it 'returns original url' do - expect(field.resource_url).not_to match(/representations/) + it 'returns true' do + expect(field.image?).to be_truthy + end end end - context 'when attachment is an image' do - let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: "test.jpg", content_type: "image/jpeg"} } + describe '#resource_url' do + context 'when calling with thumb = false' do + let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: 'test.jpg', content_type: 'image/jpeg'} } - it 'returns variant\'s url' do - expect(field.resource_url(true)).to match(/representations/) + it 'returns original url' do + expect(field.resource_url).not_to match(/representations/) + end + end + + context 'when attachment is an image' do + let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: 'test.jpg', content_type: 'image/jpeg'} } + + it 'returns variant\'s url' do + expect(field.resource_url(true)).to match(/representations/) + end + end + + context 'when attachment is not an image' do + let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: 'test.txt', content_type: 'text/plain'} } + + it 'returns original url' do + expect(field.resource_url(true)).not_to match(/representations/) + end + end + + context 'when attachment is a PDF file' do + let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: 'test.pdf', content_type: 'application/pdf'} } + before { allow(ActiveStorage::Previewer::PopplerPDFPreviewer).to receive(:accept?).and_return(true) } + + it 'returns variant\'s url' do + expect(field.resource_url(true)).to match(/representations/) + end end end - context 'when attachment is not an image' do - let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: "test.txt", content_type: "text/plain"} } + describe '#value' do + context 'when attachment exists' do + let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: 'test.jpg', content_type: 'image/jpeg'} } + + it 'returns attached object' do + expect(field.value).to be_a(ActiveStorage::Attached::One) + end + end + + context 'when attachment does not exist' do + let(:record) { FactoryBot.create :field_test } - it 'returns original url' do - expect(field.resource_url(true)).not_to match(/representations/) + it 'returns nil' do + expect(field.value).to be_nil + end end end - end - describe '#value' do - context 'when attachment exists' do - let(:record) { FactoryBot.create :field_test, active_storage_asset: {io: StringIO.new('dummy'), filename: "test.jpg", content_type: "image/jpeg"} } + describe '#eager_load' do + it 'points to associations to be eager-loaded' do + expect(field.eager_load).to eq({active_storage_asset_attachment: :blob}) + end + end - it 'returns attached object' do - expect(field.value).to be_a(ActiveStorage::Attached::One) + describe '#direct' do + let(:view) { double } + let(:field) do + RailsAdmin.config('FieldTest').fields.detect do |f| + f.name == :active_storage_asset + end.with(view: view) + end + before do + allow(view).to receive_message_chain(:main_app, :rails_direct_uploads_url) { 'http://www.example.com/rails/active_storage/direct_uploads' } + end + + context 'when false' do + it "doesn't put the direct upload url in html_attributes" do + expect(field.html_attributes[:data]&.[](:direct_upload_url)).to be_nil + end + end + + context 'when true' do + before do + RailsAdmin.config FieldTest do + field(:active_storage_asset) { direct true } + end + end + + it 'puts the direct upload url in html_attributes' do + expect(field.html_attributes[:data]&.[](:direct_upload_url)).to eq 'http://www.example.com/rails/active_storage/direct_uploads' + end end end - context 'when attachment does not exist' do - let(:record) { FactoryBot.create :field_test } + describe '#searchable' do + it 'is false' do + expect(field.searchable).to be false + end + end - it 'returns nil' do - expect(field.value).to be_nil + describe '#sortable' do + it 'is false' do + expect(field.sortable).to be false end end end -end if defined?(ActiveStorage) +end diff --git a/spec/rails_admin/config/fields/types/belongs_to_association_spec.rb b/spec/rails_admin/config/fields/types/belongs_to_association_spec.rb index 4a43150d8e..0b91ffa68d 100644 --- a/spec/rails_admin/config/fields/types/belongs_to_association_spec.rb +++ b/spec/rails_admin/config/fields/types/belongs_to_association_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::BelongsToAssociation do diff --git a/spec/rails_admin/config/fields/types/boolean_spec.rb b/spec/rails_admin/config/fields/types/boolean_spec.rb index 9543e2151e..5b654aaa54 100644 --- a/spec/rails_admin/config/fields/types/boolean_spec.rb +++ b/spec/rails_admin/config/fields/types/boolean_spec.rb @@ -1,5 +1,29 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Boolean do it_behaves_like 'a generic field type', :boolean_field, :boolean + + subject do + RailsAdmin.config(FieldTest).fields.detect do |f| + f.name == :boolean_field + end.with(object: test_object) + end + + describe '#pretty_value' do + { + false => %(), + true => %(), + nil => %(), + }.each do |field_value, expected_result| + context "when field value is '#{field_value.inspect}'" do + let(:test_object) { FieldTest.new(boolean_field: field_value) } + + it 'returns the appropriate html result' do + expect(subject.pretty_value).to eq(expected_result) + end + end + end + end end diff --git a/spec/rails_admin/config/fields/types/bson_object_id_spec.rb b/spec/rails_admin/config/fields/types/bson_object_id_spec.rb index 384f7ef4dd..40189802af 100644 --- a/spec/rails_admin/config/fields/types/bson_object_id_spec.rb +++ b/spec/rails_admin/config/fields/types/bson_object_id_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::BsonObjectId do diff --git a/spec/rails_admin/config/fields/types/carrierwave_spec.rb b/spec/rails_admin/config/fields/types/carrierwave_spec.rb index d2666423d2..da563a76f7 100644 --- a/spec/rails_admin/config/fields/types/carrierwave_spec.rb +++ b/spec/rails_admin/config/fields/types/carrierwave_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Carrierwave do diff --git a/spec/rails_admin/config/fields/types/citext_spec.rb b/spec/rails_admin/config/fields/types/citext_spec.rb new file mode 100644 index 0000000000..bdcae8e2fa --- /dev/null +++ b/spec/rails_admin/config/fields/types/citext_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RailsAdmin::Config::Fields::Types::Citext do + it_behaves_like 'a generic field type', :string_field + + it_behaves_like 'a string-like field type', :string_field +end diff --git a/spec/rails_admin/config/fields/types/ck_editor_spec.rb b/spec/rails_admin/config/fields/types/ck_editor_spec.rb index e96b769675..890d6b1afb 100644 --- a/spec/rails_admin/config/fields/types/ck_editor_spec.rb +++ b/spec/rails_admin/config/fields/types/ck_editor_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::CKEditor do diff --git a/spec/rails_admin/config/fields/types/code_mirror_spec.rb b/spec/rails_admin/config/fields/types/code_mirror_spec.rb index 8baa35c2c1..4de52d38fe 100644 --- a/spec/rails_admin/config/fields/types/code_mirror_spec.rb +++ b/spec/rails_admin/config/fields/types/code_mirror_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::CodeMirror do diff --git a/spec/rails_admin/config/fields/types/color_spec.rb b/spec/rails_admin/config/fields/types/color_spec.rb index 8bd545dfbd..5c71597ea0 100644 --- a/spec/rails_admin/config/fields/types/color_spec.rb +++ b/spec/rails_admin/config/fields/types/color_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Color do diff --git a/spec/rails_admin/config/fields/types/date_spec.rb b/spec/rails_admin/config/fields/types/date_spec.rb index ecc98d6468..5b654569c6 100644 --- a/spec/rails_admin/config/fields/types/date_spec.rb +++ b/spec/rails_admin/config/fields/types/date_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Date do diff --git a/spec/rails_admin/config/fields/types/datetime_spec.rb b/spec/rails_admin/config/fields/types/datetime_spec.rb index b48f1155ff..2df23d3f01 100644 --- a/spec/rails_admin/config/fields/types/datetime_spec.rb +++ b/spec/rails_admin/config/fields/types/datetime_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Datetime do @@ -56,7 +58,7 @@ @object = FactoryBot.create(:field_test) @object.datetime_field = field.parse_input(datetime_field: @time.strftime('%a, %d %b %Y %H:%M:%S %z')) - expect(@object.datetime_field.to_s(:rfc822)).to eq(@time.to_s(:rfc822)) + expect(@object.datetime_field.to_formatted_s(:rfc822)).to eq(@time.to_formatted_s(:rfc822)) end it 'has a customization option' do @@ -70,7 +72,7 @@ @object = FactoryBot.create(:field_test) @object.datetime_field = field.parse_input(datetime_field: @time.strftime('%Y-%m-%d %H:%M:%S')) - expect(@object.datetime_field.to_s(:rfc822)).to eq(@time.to_s(:rfc822)) + expect(@object.datetime_field.to_formatted_s(:rfc822)).to eq(@time.to_formatted_s(:rfc822)) end it 'does round-trip saving properly with non-UTC timezones' do @@ -89,14 +91,14 @@ end it 'changes formats when the locale changes' do - french_format = "%A %d %B %Y %H:%M" - allow(I18n).to receive(:t).with(:long, scope: [:time, :formats], raise: true).and_return(french_format) + french_format = '%A %d %B %Y %H:%M' + allow(I18n).to receive(:t).with(:long, scope: %i[time formats], raise: true).and_return(french_format) @object = FactoryBot.create(:field_test) @object.datetime_field = field.parse_input(datetime_field: @time.strftime(french_format)) expect(@object.datetime_field.strftime(french_format)).to eq(@time.strftime(french_format)) - american_format = "%B %d, %Y %H:%M" - allow(I18n).to receive(:t).with(:long, scope: [:time, :formats], raise: true).and_return(american_format) + american_format = '%B %d, %Y %H:%M' + allow(I18n).to receive(:t).with(:long, scope: %i[time formats], raise: true).and_return(american_format) @object = FactoryBot.create(:field_test) @object.datetime_field = field.parse_input(datetime_field: @time.strftime(american_format)) expect(@object.datetime_field.strftime(american_format)).to eq(@time.strftime(american_format)) diff --git a/spec/rails_admin/config/fields/types/decimal_spec.rb b/spec/rails_admin/config/fields/types/decimal_spec.rb index 308c4ff2d8..dad7bbac9e 100644 --- a/spec/rails_admin/config/fields/types/decimal_spec.rb +++ b/spec/rails_admin/config/fields/types/decimal_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Decimal do - it_behaves_like 'a generic field type', :decimal_field, :decimal + it_behaves_like 'a float-like field type', :float_field end diff --git a/spec/rails_admin/config/fields/types/drangonfly_spec.rb b/spec/rails_admin/config/fields/types/drangonfly_spec.rb index c0491a7b34..da8cd3c616 100644 --- a/spec/rails_admin/config/fields/types/drangonfly_spec.rb +++ b/spec/rails_admin/config/fields/types/drangonfly_spec.rb @@ -1,8 +1,33 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Dragonfly do it_behaves_like 'a generic field type', :string_field, :dragonfly + let(:field) do + RailsAdmin.config('FieldTest').fields.detect do |f| + f.name == :dragonfly_asset + end.with(object: record) + end + + describe '#image?' do + let(:file) { File.open(file_path('test.jpg')) } + let(:record) { FactoryBot.create :field_test, dragonfly_asset: file } + + it 'returns true' do + expect(field.image?).to be true + end + + context 'with non-image' do + let(:file) { File.open(file_path('test.txt')) } + + it 'returns false' do + expect(field.image?).to be false + end + end + end + describe 'with a model which does not extend Dragonfly::Model' do before do class NonDragonflyTest < Tableless diff --git a/spec/rails_admin/config/fields/types/enum_spec.rb b/spec/rails_admin/config/fields/types/enum_spec.rb index 16d12f8af6..6bee0de9b6 100644 --- a/spec/rails_admin/config/fields/types/enum_spec.rb +++ b/spec/rails_admin/config/fields/types/enum_spec.rb @@ -1,5 +1,166 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Enum do it_behaves_like 'a generic field type', :string_field, :enum + + subject { RailsAdmin.config(Team).field(:color) } + + describe "when object responds to '\#{method}_enum'" do + before do + allow_any_instance_of(Team).to receive(:color_enum).and_return(%w[blue green red]) + RailsAdmin.config Team do + edit do + field :color + end + end + end + + it 'auto-detects enumeration' do + is_expected.to be_a(RailsAdmin::Config::Fields::Types::Enum) + is_expected.not_to be_multiple + expect(subject.with(object: Team.new).enum).to eq %w[blue green red] + end + end + + describe "when class responds to '\#{method}_enum'" do + before do + allow(Team).to receive(:color_enum).and_return(%w[blue green red]) + Team.instance_eval do + def color_enum + %w[blue green red] + end + end + RailsAdmin.config Team do + edit do + field :color + end + end + end + + it 'auto-detects enumeration' do + is_expected.to be_a(RailsAdmin::Config::Fields::Types::Enum) + expect(subject.with(object: Team.new).enum).to eq %w[blue green red] + end + end + + describe 'the enum instance method' do + before do + Team.class_eval do + def color_list + %w[blue green red] + end + end + RailsAdmin.config Team do + field :color, :enum do + enum_method :color_list + end + end + end + + after do + Team.send(:remove_method, :color_list) + end + + it 'allows configuration' do + is_expected.to be_a(RailsAdmin::Config::Fields::Types::Enum) + expect(subject.with(object: Team.new).enum).to eq %w[blue green red] + end + end + + describe 'the enum class method' do + before do + Team.instance_eval do + def color_list + %w[blue green red] + end + end + RailsAdmin.config Team do + field :color, :enum do + enum_method :color_list + end + end + end + + after do + Team.instance_eval { undef :color_list } + end + + it 'allows configuration' do + is_expected.to be_a(RailsAdmin::Config::Fields::Types::Enum) + expect(subject.with(object: Team.new).enum).to eq %w[blue green red] + end + end + + describe 'when overriding enum configuration' do + before do + Team.class_eval do + def color_list + %w[blue green red] + end + end + RailsAdmin.config Team do + field :color, :enum do + enum_method :color_list + enum do + %w[yellow black] + end + end + end + end + + after do + Team.send(:remove_method, :color_list) + end + + it 'allows direct listing of enumeration options and override enum method' do + is_expected.to be_a(RailsAdmin::Config::Fields::Types::Enum) + expect(subject.with(object: Team.new).enum).to eq %w[yellow black] + end + end + + describe 'when serialize is enabled in ActiveRecord model', active_record: true do + subject { RailsAdmin.config(TeamWithSerializedEnum).field(:color) } + + before do + class TeamWithSerializedEnum < Team + self.table_name = 'teams' + if ActiveRecord.gem_version < Gem::Version.new('7.1') + serialize :color + else + serialize :color, coder: JSON + end + def color_enum + %w[blue green red] + end + end + RailsAdmin.config do |c| + c.included_models = [TeamWithSerializedEnum] + end + end + + it 'makes enumeration multi-selectable' do + is_expected.to be_multiple + end + end + + describe 'when serialize is enabled in Mongoid model', mongoid: true do + before do + allow(Team).to receive(:color_enum).and_return(%w[blue green red]) + Team.instance_eval do + field :color, type: Array + end + end + + after do + Team.instance_eval do + field :color, type: String + end + end + + it 'makes enumeration multi-selectable' do + is_expected.to be_multiple + end + end end diff --git a/spec/rails_admin/config/fields/types/file_upload_spec.rb b/spec/rails_admin/config/fields/types/file_upload_spec.rb index 3b310bec29..35ffb6280c 100644 --- a/spec/rails_admin/config/fields/types/file_upload_spec.rb +++ b/spec/rails_admin/config/fields/types/file_upload_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::FileUpload do @@ -12,20 +14,24 @@ field :paperclip_asset do delete_method :delete_paperclip_asset end - field :active_storage_asset do - delete_method :remove_active_storage_asset - end if defined?(ActiveStorage) - field :shrine_asset do - delete_method :remove_shrine_asset - cache_method :cached_shrine_asset_data - end if defined?(Shrine) + if defined?(ActiveStorage) + field :active_storage_asset do + delete_method :remove_active_storage_asset + end + end + if defined?(Shrine) + field :shrine_asset do + delete_method :remove_shrine_asset + cache_method :cached_shrine_asset_data + end + end end end - expect(RailsAdmin.config(FieldTest).field(:carrierwave_asset).allowed_methods.collect(&:to_s)).to eq %w(carrierwave_asset remove_carrierwave_asset carrierwave_asset_cache) - expect(RailsAdmin.config(FieldTest).field(:dragonfly_asset).allowed_methods.collect(&:to_s)).to eq %w(dragonfly_asset remove_dragonfly_asset retained_dragonfly_asset) - expect(RailsAdmin.config(FieldTest).field(:paperclip_asset).allowed_methods.collect(&:to_s)).to eq %w(paperclip_asset delete_paperclip_asset) - expect(RailsAdmin.config(FieldTest).field(:active_storage_asset).allowed_methods.collect(&:to_s)).to eq %w(active_storage_asset remove_active_storage_asset) if defined?(ActiveStorage) - expect(RailsAdmin.config(FieldTest).field(:shrine_asset).allowed_methods.collect(&:to_s)).to eq %w(shrine_asset remove_shrine_asset cached_shrine_asset_data) if defined?(Shrine) + expect(RailsAdmin.config(FieldTest).field(:carrierwave_asset).allowed_methods.collect(&:to_s)).to eq %w[carrierwave_asset remove_carrierwave_asset carrierwave_asset_cache] + expect(RailsAdmin.config(FieldTest).field(:dragonfly_asset).allowed_methods.collect(&:to_s)).to eq %w[dragonfly_asset remove_dragonfly_asset retained_dragonfly_asset] + expect(RailsAdmin.config(FieldTest).field(:paperclip_asset).allowed_methods.collect(&:to_s)).to eq %w[paperclip_asset delete_paperclip_asset] + expect(RailsAdmin.config(FieldTest).field(:active_storage_asset).allowed_methods.collect(&:to_s)).to eq %w[active_storage_asset remove_active_storage_asset] if defined?(ActiveStorage) + expect(RailsAdmin.config(FieldTest).field(:shrine_asset).allowed_methods.collect(&:to_s)).to eq %w[shrine_asset remove_shrine_asset cached_shrine_asset_data] if defined?(Shrine) end end @@ -77,4 +83,57 @@ def resource_url end end end + + describe '#image?' do + let(:filename) { 'dummy.txt' } + let :rails_admin_field do + RailsAdmin.config('FieldTest').fields.detect do |f| + f.name == :string_field + end.with( + object: FieldTest.new(string_field: filename), + view: ApplicationController.new.view_context, + ) + end + before do + RailsAdmin.config FieldTest do + field :string_field, :file_upload do + def resource_url + "http://example.com/#{value}" + end + end + end + end + + context 'when the file is not an image' do + let(:filename) { 'dummy.txt' } + + it 'returns false' do + expect(rails_admin_field.image?).to be false + end + end + + context 'when the file is an image' do + let(:filename) { 'dummy.jpg' } + + it 'returns true' do + expect(rails_admin_field.image?).to be true + end + end + + context 'when the file is an image but suffixed with a query string' do + let(:filename) { 'dummy.jpg?foo=bar' } + + it 'returns true' do + expect(rails_admin_field.image?).to be true + end + end + + context "when the filename can't be represented as a valid URI" do + let(:filename) { 'du mmy.jpg' } + + it 'returns false' do + expect(rails_admin_field.image?).to be false + end + end + end end diff --git a/spec/rails_admin/config/fields/types/float_spec.rb b/spec/rails_admin/config/fields/types/float_spec.rb index 83ada22706..6406b2f036 100644 --- a/spec/rails_admin/config/fields/types/float_spec.rb +++ b/spec/rails_admin/config/fields/types/float_spec.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Float do - it_behaves_like 'a generic field type', :float_field, :float + it_behaves_like 'a float-like field type', :float_field end diff --git a/spec/rails_admin/config/fields/types/froala_spec.rb b/spec/rails_admin/config/fields/types/froala_spec.rb index 19d207bf19..190eafd898 100644 --- a/spec/rails_admin/config/fields/types/froala_spec.rb +++ b/spec/rails_admin/config/fields/types/froala_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Froala do diff --git a/spec/rails_admin/config/fields/types/has_and_belongs_to_many_association_spec.rb b/spec/rails_admin/config/fields/types/has_and_belongs_to_many_association_spec.rb index 70b6c6bbf6..a387ed9d93 100644 --- a/spec/rails_admin/config/fields/types/has_and_belongs_to_many_association_spec.rb +++ b/spec/rails_admin/config/fields/types/has_and_belongs_to_many_association_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::HasAndBelongsToManyAssociation do diff --git a/spec/rails_admin/config/fields/types/has_many_association_spec.rb b/spec/rails_admin/config/fields/types/has_many_association_spec.rb index b1173a9fc9..1011b4f791 100644 --- a/spec/rails_admin/config/fields/types/has_many_association_spec.rb +++ b/spec/rails_admin/config/fields/types/has_many_association_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::HasManyAssociation do diff --git a/spec/rails_admin/config/fields/types/has_one_association_spec.rb b/spec/rails_admin/config/fields/types/has_one_association_spec.rb index 78881e1640..babf13a3df 100644 --- a/spec/rails_admin/config/fields/types/has_one_association_spec.rb +++ b/spec/rails_admin/config/fields/types/has_one_association_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::HasOneAssociation do diff --git a/spec/rails_admin/config/fields/types/hidden_spec.rb b/spec/rails_admin/config/fields/types/hidden_spec.rb index ca60e2fdc0..cfa5273c3f 100644 --- a/spec/rails_admin/config/fields/types/hidden_spec.rb +++ b/spec/rails_admin/config/fields/types/hidden_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Hidden do diff --git a/spec/rails_admin/config/fields/types/inet_spec.rb b/spec/rails_admin/config/fields/types/inet_spec.rb index 067a28765e..0e3509cef2 100644 --- a/spec/rails_admin/config/fields/types/inet_spec.rb +++ b/spec/rails_admin/config/fields/types/inet_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Inet do diff --git a/spec/rails_admin/config/fields/types/integer_spec.rb b/spec/rails_admin/config/fields/types/integer_spec.rb index 797f10c3ba..b806f50ff9 100644 --- a/spec/rails_admin/config/fields/types/integer_spec.rb +++ b/spec/rails_admin/config/fields/types/integer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Integer do diff --git a/spec/rails_admin/config/fields/types/json_spec.rb b/spec/rails_admin/config/fields/types/json_spec.rb index a773036393..a1e0395827 100644 --- a/spec/rails_admin/config/fields/types/json_spec.rb +++ b/spec/rails_admin/config/fields/types/json_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Json do @@ -26,12 +28,12 @@ end it 'retuns correct value' do - allow(object).to receive(:json_field) { {sample_key: "sample_value"} } + allow(object).to receive(:json_field) { {sample_key: 'sample_value'} } actual = field.with(bindings).formatted_value expected = [ - "{", - " \"sample_key\": \"sample_value\"", - "}", + '{', + ' "sample_key": "sample_value"', + '}', ].join("\n") expect(actual).to eq(expected) end @@ -47,12 +49,12 @@ end it 'retuns correct value' do - allow(object).to receive(:json_field) { {sample_key: "sample_value"} } + allow(object).to receive(:json_field) { {sample_key: 'sample_value'} } actual = field.with(bindings).pretty_value expected = [ - "
    {",
    -        "  "sample_key": "sample_value"",
    -        "}
    ", + '
    {',
    +        '  "sample_key": "sample_value"',
    +        '}
    ', ].join("\n") expect(actual).to eq(expected) end @@ -74,12 +76,12 @@ end it 'returns correct value' do - allow(object).to receive(:json_field) { {sample_key: "sample_value"} } + allow(object).to receive(:json_field) { {sample_key: 'sample_value'} } actual = field.with(bindings).export_value expected = [ - "{", - " \"sample_key\": \"sample_value\"", - "}", + '{', + ' "sample_key": "sample_value"', + '}', ].join("\n") expect(actual).to eq(expected) end diff --git a/spec/rails_admin/config/fields/types/multiple_active_storage_spec.rb b/spec/rails_admin/config/fields/types/multiple_active_storage_spec.rb index 72808d14f0..39cc09fd36 100644 --- a/spec/rails_admin/config/fields/types/multiple_active_storage_spec.rb +++ b/spec/rails_admin/config/fields/types/multiple_active_storage_spec.rb @@ -1,95 +1,161 @@ +# frozen_string_literal: true + require 'spec_helper' -RSpec.describe RailsAdmin::Config::Fields::Types::MultipleActiveStorage do - it_behaves_like 'a generic field type', :string_field, :multiple_active_storage - - let(:record) { FactoryBot.create :field_test } - let(:field) do - RailsAdmin.config('FieldTest').fields.detect do |f| - f.name == :active_storage_assets - end.with( - object: record, - view: ApplicationController.new.view_context, - ) - end +if defined?(ActiveStorage) + RSpec.describe RailsAdmin::Config::Fields::Types::MultipleActiveStorage do + it_behaves_like 'a generic field type', :string_field, :multiple_active_storage + + let(:record) { FactoryBot.create :field_test } + let(:field) do + RailsAdmin.config('FieldTest').fields.detect do |f| + f.name == :active_storage_assets + end.with( + object: record, + view: ApplicationController.new.view_context, + ) + end - describe RailsAdmin::Config::Fields::Types::MultipleActiveStorage::ActiveStorageAttachment do - describe '#thumb_method' do - let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: "test.txt", content_type: "text/plain"}] } - subject { field.attachments[0] } + describe RailsAdmin::Config::Fields::Types::MultipleActiveStorage::ActiveStorageAttachment do + describe '#thumb_method' do + let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.txt', content_type: 'text/plain'}] } + subject { field.attachments[0] } - it 'returns corresponding value which is to be passed to image_processing(ActiveStorage >= 6.0) or mini_magick(ActiveStorage 5.2)' do - if ::ActiveStorage::VERSION::MAJOR >= 6 + it 'returns corresponding value which is to be passed to image_processing(ActiveStorage >= 6.0) or mini_magick(ActiveStorage 5.2)' do expect(subject.thumb_method).to eq(resize_to_limit: [100, 100]) - else - expect(subject.thumb_method).to eq(resize: '100x100>') end end - end - describe '#pretty_value' do - subject { field.pretty_value } + describe '#pretty_value' do + subject { field.pretty_value } - context 'when attachment is not an image' do - let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: "test.txt", content_type: "text/plain"}] } + context 'when attachment is not an image' do + let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.txt', content_type: 'text/plain'}] } - it 'uses filename as link text' do - expect(Nokogiri::HTML(subject).text).to eq 'test.txt' + it 'uses filename as link text' do + expect(Nokogiri::HTML(subject).text).to eq 'test.txt' + end end - end - context 'when the field is an image' do - let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: "test.jpg", content_type: "image/jpeg"}] } + context 'when the field is an image' do + let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.jpg', content_type: 'image/jpeg'}] } - it 'shows thumbnail image with a link' do - expect(Nokogiri::HTML(subject).css('img').attribute('src').value).to match(%r{rails/active_storage/representations}) - expect(Nokogiri::HTML(subject).css('a').attribute('href').value).to match(%r{rails/active_storage/blobs}) + it 'shows thumbnail image with a link' do + expect(Nokogiri::HTML(subject).css('img').attribute('src').value).to match(%r{rails/active_storage/representations}) + expect(Nokogiri::HTML(subject).css('a').attribute('href').value).to match(%r{rails/active_storage/blobs}) + end end end - end - describe '#image?' do - context 'when attachment is an image' do - let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: "test.jpg", content_type: "image/jpeg"}] } + describe '#image?' do + context 'when attachment is an image' do + let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.jpg', content_type: 'image/jpeg'}] } + + it 'returns true' do + expect(field.attachments[0].image?).to be_truthy + end + end + + context 'when attachment is not an image' do + let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.txt', content_type: 'text/plain'}] } + + it 'returns false' do + expect(field.attachments[0].image?).to be_falsy + end + end + + context 'when attachment is a PDF file' do + let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.pdf', content_type: 'application/pdf'}] } + before { allow(ActiveStorage::Previewer::PopplerPDFPreviewer).to receive(:accept?).and_return(true) } - it 'returns true' do - expect(field.attachments[0].image?).to be_truthy + it 'returns true' do + expect(field.attachments[0].image?).to be_truthy + end end end - context 'when attachment is not an image' do - let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: "test.txt", content_type: "text/plain"}] } + describe '#resource_url' do + context 'when calling with thumb = false' do + let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.jpg', content_type: 'image/jpeg'}] } - it 'returns false' do - expect(field.attachments[0].image?).to be_falsy + it 'returns original url' do + expect(field.attachments[0].resource_url).not_to match(/representations/) + end + end + + context 'when attachment is an image' do + let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.jpg', content_type: 'image/jpeg'}] } + + it 'returns variant\'s url' do + expect(field.attachments[0].resource_url(true)).to match(/representations/) + end + end + + context 'when attachment is not an image' do + let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.txt', content_type: 'text/plain'}] } + + it 'returns original url' do + expect(field.attachments[0].resource_url(true)).not_to match(/representations/) + end + end + + context 'when attachment is a PDF file' do + let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: 'test.pdf', content_type: 'application/pdf'}] } + before { allow(ActiveStorage::Previewer::PopplerPDFPreviewer).to receive(:accept?).and_return(true) } + + it 'returns variant\'s url' do + expect(field.attachments[0].resource_url(true)).to match(/representations/) + end end end end - describe '#resource_url' do - context 'when calling with thumb = false' do - let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: "test.jpg", content_type: "image/jpeg"}] } + describe '#eager_load' do + it 'points to associations to be eager-loaded' do + expect(field.eager_load).to eq({active_storage_assets_attachments: :blob}) + end + end - it 'returns original url' do - expect(field.attachments[0].resource_url).not_to match(/representations/) + describe '#direct' do + let(:view) { double } + let(:field) do + RailsAdmin.config('FieldTest').fields.detect do |f| + f.name == :active_storage_assets + end.with(view: view) + end + before do + allow(view).to receive_message_chain(:main_app, :rails_direct_uploads_url) { 'http://www.example.com/rails/active_storage/direct_uploads' } + end + + context 'when false' do + it "doesn't put the direct upload url in html_attributes" do + expect(field.html_attributes[:data]&.[](:direct_upload_url)).to be_nil end end - context 'when attachment is an image' do - let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: "test.jpg", content_type: "image/jpeg"}] } + context 'when true' do + before do + RailsAdmin.config FieldTest do + field(:active_storage_assets) { direct true } + end + end - it 'returns variant\'s url' do - expect(field.attachments[0].resource_url(true)).to match(/representations/) + it 'puts the direct upload url in html_attributes' do + expect(field.html_attributes[:data]&.[](:direct_upload_url)).to eq 'http://www.example.com/rails/active_storage/direct_uploads' end end + end - context 'when attachment is not an image' do - let(:record) { FactoryBot.create :field_test, active_storage_assets: [{io: StringIO.new('dummy'), filename: "test.txt", content_type: "text/plain"}] } + describe '#searchable' do + it 'is false' do + expect(field.searchable).to be false + end + end - it 'returns original url' do - expect(field.attachments[0].resource_url(true)).not_to match(/representations/) - end + describe '#sortable' do + it 'is false' do + expect(field.sortable).to be false end end end -end if defined?(ActiveStorage) +end diff --git a/spec/rails_admin/config/fields/types/multiple_carrierwave_spec.rb b/spec/rails_admin/config/fields/types/multiple_carrierwave_spec.rb index a4d710520c..979d3110c8 100644 --- a/spec/rails_admin/config/fields/types/multiple_carrierwave_spec.rb +++ b/spec/rails_admin/config/fields/types/multiple_carrierwave_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'base64' @@ -6,7 +8,6 @@ describe '#thumb_method' do before do - allow_any_instance_of(CarrierwaveUploader).to receive(:cache!) RailsAdmin.config FieldTest do field :carrierwave_assets, :multiple_carrierwave end diff --git a/spec/rails_admin/config/fields/types/multiple_file_upload_spec.rb b/spec/rails_admin/config/fields/types/multiple_file_upload_spec.rb index 9b294d0a38..fb24106cb3 100644 --- a/spec/rails_admin/config/fields/types/multiple_file_upload_spec.rb +++ b/spec/rails_admin/config/fields/types/multiple_file_upload_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::MultipleFileUpload do @@ -8,13 +10,15 @@ RailsAdmin.config do |config| config.model FieldTest do field :carrierwave_assets, :multiple_carrierwave - field :active_storage_assets, :multiple_active_storage do - delete_method :remove_active_storage_assets - end if defined?(ActiveStorage) + if defined?(ActiveStorage) + field :active_storage_assets, :multiple_active_storage do + delete_method :remove_active_storage_assets + end + end end end - expect(RailsAdmin.config(FieldTest).field(:carrierwave_assets).with(object: FieldTest.new).allowed_methods.collect(&:to_s)).to eq %w(carrierwave_assets) - expect(RailsAdmin.config(FieldTest).field(:active_storage_assets).with(object: FieldTest.new).allowed_methods.collect(&:to_s)).to eq %w(active_storage_assets remove_active_storage_assets) if defined?(ActiveStorage) + expect(RailsAdmin.config(FieldTest).field(:carrierwave_assets).with(object: FieldTest.new).allowed_methods.collect(&:to_s)).to eq %w[carrierwave_assets] + expect(RailsAdmin.config(FieldTest).field(:active_storage_assets).with(object: FieldTest.new).allowed_methods.collect(&:to_s)).to eq %w[active_storage_assets remove_active_storage_assets] if defined?(ActiveStorage) end end @@ -142,4 +146,59 @@ def value expect(rails_admin_field.with(object: FieldTest.new(string_field: 'dummy.txt')).attachments.map(&:value)).to eq ['dummy.txt'] end end + + describe '#image?' do + let(:filename) { 'dummy.txt' } + let :rails_admin_field do + RailsAdmin.config('FieldTest').fields.detect do |f| + f.name == :string_field + end.with( + object: FieldTest.new(string_field: filename), + view: ApplicationController.new.view_context, + ) + end + before do + RailsAdmin.config FieldTest do + field :string_field, :multiple_file_upload do + attachment do + def resource_url + "http://example.com/#{value}" + end + end + end + end + end + + context 'when the file is not an image' do + let(:filename) { 'dummy.txt' } + + it 'returns false' do + expect(rails_admin_field.attachments.first.image?).to be false + end + end + + context 'when the file is an image' do + let(:filename) { 'dummy.jpg' } + + it 'returns true' do + expect(rails_admin_field.attachments.first.image?).to be true + end + end + + context 'when the file is an image but suffixed with a query string' do + let(:filename) { 'dummy.jpg?foo=bar' } + + it 'returns true' do + expect(rails_admin_field.attachments.first.image?).to be true + end + end + + context "when the filename can't be represented as a valid URI" do + let(:filename) { 'du mmy.jpg' } + + it 'returns false' do + expect(rails_admin_field.attachments.first.image?).to be false + end + end + end end diff --git a/spec/rails_admin/config/fields/types/numeric_spec.rb b/spec/rails_admin/config/fields/types/numeric_spec.rb new file mode 100644 index 0000000000..9be08ac54a --- /dev/null +++ b/spec/rails_admin/config/fields/types/numeric_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RailsAdmin::Config::Fields::Types::Numeric do + it_behaves_like 'a generic field type', :integer_field, :integer + + subject do + RailsAdmin.config('FieldTest').fields.detect do |f| + f.name == :integer_field + end.with(object: FieldTest.new) + end + + describe '#view_helper' do + it "uses the 'number' type input tag" do + expect(subject.view_helper).to eq(:number_field) + end + end +end diff --git a/spec/rails_admin/config/fields/types/paperclip_spec.rb b/spec/rails_admin/config/fields/types/paperclip_spec.rb index 168682f36a..9b9b5836e2 100644 --- a/spec/rails_admin/config/fields/types/paperclip_spec.rb +++ b/spec/rails_admin/config/fields/types/paperclip_spec.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Paperclip do it_behaves_like 'a generic field type', :string_field, :paperclip - context 'when a *_file_name field exists but not decleared as has_attached_file' do + context 'when a *_file_name field exists but not declared as has_attached_file' do before do class PaperclipTest < Tableless column :some_file_name, :varchar diff --git a/spec/rails_admin/config/fields/types/password_spec.rb b/spec/rails_admin/config/fields/types/password_spec.rb index 979bf89399..01201fbac0 100644 --- a/spec/rails_admin/config/fields/types/password_spec.rb +++ b/spec/rails_admin/config/fields/types/password_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Password do diff --git a/spec/rails_admin/config/fields/types/serialized_spec.rb b/spec/rails_admin/config/fields/types/serialized_spec.rb index feb30b2e4c..86df0b1caf 100644 --- a/spec/rails_admin/config/fields/types/serialized_spec.rb +++ b/spec/rails_admin/config/fields/types/serialized_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Serialized do diff --git a/spec/rails_admin/config/fields/types/shrine_spec.rb b/spec/rails_admin/config/fields/types/shrine_spec.rb index 091ea41123..b7e01848a3 100644 --- a/spec/rails_admin/config/fields/types/shrine_spec.rb +++ b/spec/rails_admin/config/fields/types/shrine_spec.rb @@ -1,147 +1,151 @@ -require 'spec_helper' - -RSpec.describe RailsAdmin::Config::Fields::Types::Shrine do - context 'when asset is an image with versions' do - let(:record) { FactoryBot.create :field_test, shrine_versioning_asset: FakeIO.new('dummy', filename: 'test.jpg', content_type: 'image/jpeg') } +# frozen_string_literal: true - let(:field) do - RailsAdmin.config('FieldTest').fields.detect do |f| - f.name == :shrine_versioning_asset - end.with(object: record) - end +require 'spec_helper' - before do - if Gem.loaded_specs['shrine'].version >= Gem::Version.create('3') - if record.shrine_versioning_asset - record.shrine_versioning_asset_derivatives! - record.save +if defined?(Shrine) + RSpec.describe RailsAdmin::Config::Fields::Types::Shrine do + context 'when asset is an image with versions' do + let(:record) { FactoryBot.create :field_test, shrine_versioning_asset: FakeIO.new('dummy', filename: 'test.jpg', content_type: 'image/jpeg') } + + let(:field) do + RailsAdmin.config('FieldTest').fields.detect do |f| + f.name == :shrine_versioning_asset + end.with(object: record) + end + + before do + if Gem.loaded_specs['shrine'].version >= Gem::Version.create('3') + if record.shrine_versioning_asset + record.shrine_versioning_asset_derivatives! + record.save + end + else + skip end - else - skip end - end - describe '#image?' do - it 'returns true' do - expect(field.image?).to be_truthy + describe '#image?' do + it 'returns true' do + expect(field.image?).to be_truthy + end end - end - describe '#link_name' do - it 'returns filename' do - expect(field.link_name).to eq('test.jpg') + describe '#link_name' do + it 'returns filename' do + expect(field.link_name).to eq('test.jpg') + end end - end - describe '#value' do - context 'when attachment exists' do - it 'returns attached object' do - expect(field.value).to be_a(ShrineVersioningUploader::UploadedFile) + describe '#value' do + context 'when attachment exists' do + it 'returns attached object' do + expect(field.value).to be_a(ShrineVersioningUploader::UploadedFile) + end end - end - context 'when attachment does not exist' do - let(:record) { FactoryBot.create :field_test } + context 'when attachment does not exist' do + let(:record) { FactoryBot.create :field_test } - it 'returns nil' do - expect(field.value).to be_nil + it 'returns nil' do + expect(field.value).to be_nil + end end end - end - describe '#thumb_method' do - it 'returns :thumb' do - expect(field.thumb_method).to eq(:thumb) + describe '#thumb_method' do + it 'returns :thumb' do + expect(field.thumb_method).to eq(:thumb) + end end - end - describe '#resource_url' do - context 'when calling without thumb' do - it 'returns original url' do - expect(field.resource_url).to_not match(/thumb-/) + describe '#resource_url' do + context 'when calling without thumb' do + it 'returns original url' do + expect(field.resource_url).to_not match(/thumb-/) + end end - end - context 'when calling with thumb' do - it 'returns thumb url' do - expect(field.resource_url(field.thumb_method)).to match(/thumb-/) + context 'when calling with thumb' do + it 'returns thumb url' do + expect(field.resource_url(field.thumb_method)).to match(/thumb-/) + end end end end - end - context 'when asset without versions' do - let(:record) { FactoryBot.create :field_test } - let(:field) do - RailsAdmin.config('FieldTest').fields.detect do |f| - f.name == :shrine_asset - end.with(object: record) - end + context 'when asset without versions' do + let(:record) { FactoryBot.create :field_test } + let(:field) do + RailsAdmin.config('FieldTest').fields.detect do |f| + f.name == :shrine_asset + end.with(object: record) + end - describe '#image?' do - context 'when attachment is an image' do - let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.jpg', content_type: 'image/jpeg') } + describe '#image?' do + context 'when attachment is an image' do + let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.jpg', content_type: 'image/jpeg') } - it 'returns true' do - expect(field.image?).to be_truthy + it 'returns true' do + expect(field.image?).to be_truthy + end end - end - context 'when attachment is not an image' do - let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.txt', content_type: 'text/plain') } + context 'when attachment is not an image' do + let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.txt', content_type: 'text/plain') } - it 'returns false' do - expect(field.image?).to be_falsy + it 'returns false' do + expect(field.image?).to be_falsy + end end end - end - describe '#value' do - context 'when attachment exists' do - let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.txt', content_type: 'text/plain') } + describe '#value' do + context 'when attachment exists' do + let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.txt', content_type: 'text/plain') } - it 'returns attached object' do - expect(field.value).to be_a(ShrineUploader::UploadedFile) + it 'returns attached object' do + expect(field.value).to be_a(ShrineUploader::UploadedFile) + end end - end - context 'when attachment does not exist' do - it 'returns nil' do - expect(field.value).to be_nil + context 'when attachment does not exist' do + it 'returns nil' do + expect(field.value).to be_nil + end end end - end - describe '#thumb_method' do - let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.jpg', content_type: 'image/jpeg') } + describe '#thumb_method' do + let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.jpg', content_type: 'image/jpeg') } - it 'returns nil' do - expect(field.thumb_method).to eq(nil) + it 'returns nil' do + expect(field.thumb_method).to eq(nil) + end end - end - describe '#resource_url' do - context 'when calling without thumb' do - let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.txt', content_type: 'text/plain') } + describe '#resource_url' do + context 'when calling without thumb' do + let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.txt', content_type: 'text/plain') } - it 'returns url' do - expect(field.resource_url).not_to be_nil + it 'returns url' do + expect(field.resource_url).not_to be_nil + end end - end - context 'when calling with thumb' do - let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.jpg', content_type: 'image/jpeg') } + context 'when calling with thumb' do + let(:record) { FactoryBot.create :field_test, shrine_asset: FakeIO.new('dummy', filename: 'test.jpg', content_type: 'image/jpeg') } - it 'returns url' do - expect(field.resource_url(field.thumb_method)).not_to be_nil + it 'returns url' do + expect(field.resource_url(field.thumb_method)).not_to be_nil + end end - end - context 'when attachment does not exist' do - it 'returns nil' do - expect(field.resource_url).to be_nil + context 'when attachment does not exist' do + it 'returns nil' do + expect(field.resource_url).to be_nil + end end end end end -end if defined?(Shrine) +end diff --git a/spec/rails_admin/config/fields/types/simple_mde_spec.rb b/spec/rails_admin/config/fields/types/simple_mde_spec.rb index 6739e412b5..844f6f732a 100644 --- a/spec/rails_admin/config/fields/types/simple_mde_spec.rb +++ b/spec/rails_admin/config/fields/types/simple_mde_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::SimpleMDE do diff --git a/spec/rails_admin/config/fields/types/string_like_spec.rb b/spec/rails_admin/config/fields/types/string_like_spec.rb index c4c5fb3648..ff137dc665 100644 --- a/spec/rails_admin/config/fields/types/string_like_spec.rb +++ b/spec/rails_admin/config/fields/types/string_like_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::StringLike do diff --git a/spec/rails_admin/config/fields/types/string_spec.rb b/spec/rails_admin/config/fields/types/string_spec.rb index 49b95a1e9b..fc1ed49e25 100644 --- a/spec/rails_admin/config/fields/types/string_spec.rb +++ b/spec/rails_admin/config/fields/types/string_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::String do diff --git a/spec/rails_admin/config/fields/types/text_spec.rb b/spec/rails_admin/config/fields/types/text_spec.rb index e95cac06b6..c3b7b4f20d 100644 --- a/spec/rails_admin/config/fields/types/text_spec.rb +++ b/spec/rails_admin/config/fields/types/text_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Text do diff --git a/spec/rails_admin/config/fields/types/time_spec.rb b/spec/rails_admin/config/fields/types/time_spec.rb index ceb0ec5746..c7a0cd24fc 100644 --- a/spec/rails_admin/config/fields/types/time_spec.rb +++ b/spec/rails_admin/config/fields/types/time_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + require 'spec_helper' -RSpec.describe RailsAdmin::Config::Fields::Types::Time do +RSpec.describe RailsAdmin::Config::Fields::Types::Time, active_record: true do it_behaves_like 'a generic field type', :time_field, :time describe '#parse_input' do @@ -18,7 +20,7 @@ before :each do @object = FactoryBot.create(:field_test) - @time = ::Time.now.getutc + @time = ::Time.new(2000, 1, 1, 3, 45) end after :each do @@ -30,10 +32,10 @@ expect(@object.time_field.strftime('%H:%M')).to eq(@time.strftime('%H:%M')) end - it 'interprets time value as UTC when timezone is specified' do + it 'interprets time value as local time when timezone is specified' do Time.zone = 'Eastern Time (US & Canada)' # -05:00 - @object.time_field = field.parse_input(time_field: @time.strftime('%H:%M')) - expect(@object.time_field.utc.strftime('%H:%M')).to eq(@time.strftime('%H:%M')) + @object.time_field = field.parse_input(time_field: '2000-01-01T03:45:00') + expect(@object.time_field.strftime('%H:%M')).to eq('03:45') end context 'with a custom strftime_format' do diff --git a/spec/rails_admin/config/fields/types/timestamp_spec.rb b/spec/rails_admin/config/fields/types/timestamp_spec.rb index e2afb91a0a..e4aead1bca 100644 --- a/spec/rails_admin/config/fields/types/timestamp_spec.rb +++ b/spec/rails_admin/config/fields/types/timestamp_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Timestamp, active_record: true do diff --git a/spec/rails_admin/config/fields/types/uuid_spec.rb b/spec/rails_admin/config/fields/types/uuid_spec.rb index c6a752697f..bebe33e231 100644 --- a/spec/rails_admin/config/fields/types/uuid_spec.rb +++ b/spec/rails_admin/config/fields/types/uuid_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Uuid do diff --git a/spec/rails_admin/config/fields/types/wysihtml5_spec.rb b/spec/rails_admin/config/fields/types/wysihtml5_spec.rb index 0811459c00..ea8f4d8649 100644 --- a/spec/rails_admin/config/fields/types/wysihtml5_spec.rb +++ b/spec/rails_admin/config/fields/types/wysihtml5_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields::Types::Wysihtml5 do diff --git a/spec/rails_admin/config/fields_spec.rb b/spec/rails_admin/config/fields_spec.rb index d9602b972b..a87be37e6c 100644 --- a/spec/rails_admin/config/fields_spec.rb +++ b/spec/rails_admin/config/fields_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Fields, mongoid: true do diff --git a/spec/rails_admin/config/has_description_spec.rb b/spec/rails_admin/config/has_description_spec.rb index 67897fdcae..26d0f6f904 100644 --- a/spec/rails_admin/config/has_description_spec.rb +++ b/spec/rails_admin/config/has_description_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::HasDescription do diff --git a/spec/rails_admin/config/has_fields_spec.rb b/spec/rails_admin/config/has_fields_spec.rb index 613398fbec..37528bcde6 100644 --- a/spec/rails_admin/config/has_fields_spec.rb +++ b/spec/rails_admin/config/has_fields_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::HasFields do @@ -24,7 +26,7 @@ end end expect { RailsAdmin.config(Team).fields.detect { |f| f.name == :division } }.not_to raise_error - expect { RailsAdmin.config(Team).fields.detect { |f| f.name == :division }.visible? }.to raise_error("undefined method `[]' for nil:NilClass") + expect { RailsAdmin.config(Team).fields.detect { |f| f.name == :division }.visible? }.to raise_error(/undefined method `\[\]' for nil/) end it 'assigns properties to new one on overriding existing field' do @@ -36,19 +38,59 @@ expect(RailsAdmin.config(Team).fields.detect { |f| f.name == :players }.properties).not_to be_nil end - it 'does not change the order of existing fields, if some field types of them are changed' do - original_fields_order = RailsAdmin.config(Team).fields.map(&:name) + describe '#configure' do + it 'does not change the order of existing fields, if some field types of them are changed' do + original_fields_order = RailsAdmin.config(Team).fields.map(&:name) - RailsAdmin.config do |config| - config.model Team do - configure :players, :enum do - enum { [] } + RailsAdmin.config do |config| + config.model Team do + configure :players, :enum do + enum { [] } + end + + configure :revenue, :integer + end + end + + expect(RailsAdmin.config(Team).fields.map(&:name)).to eql(original_fields_order) + end + + it 'allows passing multiple fields to share the same configuration' do + target_field_names = %i[players revenue] + + original_config = RailsAdmin.config(Team).fields.select { |field| target_field_names.include?(field.name) } + original_config.each { |field| expect(field).to be_visible } + + RailsAdmin.config do |config| + config.model Team do + configure target_field_names do + visible false + end end + end + + updated_config = RailsAdmin.config(Team).fields.select { |field| target_field_names.include?(field.name) } + updated_config.each { |field| expect(field).to_not be_visible } + end + end - configure :revenue, :integer + describe '#_fields' do + let(:config) { RailsAdmin.config(Team) } + before do + RailsAdmin.config(Team) do + field :id + field :wins, :boolean end end - expect(RailsAdmin.config(Team).fields.map(&:name)).to eql(original_fields_order) + it "does not cause FrozenError by changing exiting field's type" do + # Reference the fields for readonly + config.edit.send(:_fields, true) + + RailsAdmin.config(Team) do + field :wins, :integer + end + expect(config.fields.map(&:name)).to match_array %i[id wins] + end end end diff --git a/spec/rails_admin/config/lazy_model_spec.rb b/spec/rails_admin/config/lazy_model_spec.rb index 4fed5b2101..1a2d23a0aa 100644 --- a/spec/rails_admin/config/lazy_model_spec.rb +++ b/spec/rails_admin/config/lazy_model_spec.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::LazyModel do subject { RailsAdmin::Config::LazyModel.new(:Team, &block) } let(:block) { proc { register_instance_option('parameter') } } # an arbitrary instance method we can spy on - describe '#store' do + describe '#initialize' do it "doesn't evaluate the block immediately" do expect_any_instance_of(RailsAdmin::Config::Model).not_to receive(:register_instance_option) subject @@ -23,6 +25,21 @@ end end + describe '#add_deferred_block' do + let(:another_block) { proc { register_instance_option('parameter2') } } + + it "doesn't evaluate the block immediately" do + expect_any_instance_of(RailsAdmin::Config::Model).not_to receive(:register_instance_option).with('parameter2') + subject.add_deferred_block(&another_block) + end + + it 'evaluates the block immediately after initialization' do + subject.target + expect_any_instance_of(RailsAdmin::Config::Model).to receive(:register_instance_option).with('parameter2') + subject.add_deferred_block(&another_block) + end + end + context 'when a method is defined in Kernel' do before do Kernel.module_eval do diff --git a/spec/rails_admin/config/model_spec.rb b/spec/rails_admin/config/model_spec.rb index c0434a498f..c3e6534231 100644 --- a/spec/rails_admin/config/model_spec.rb +++ b/spec/rails_admin/config/model_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Model do @@ -9,8 +11,21 @@ end it 'returns false when included, true otherwise' do - expect(RailsAdmin.config(Player).excluded?).to be_truthy - expect(RailsAdmin.config(Comment).excluded?).to be_falsey + allow(RailsAdmin::AbstractModel).to receive(:all).and_call_original + + player_config = RailsAdmin.config(Player) + expect(player_config.excluded?).to be_truthy + expect(RailsAdmin::AbstractModel).to have_received(:all).once + # Calling a second time uses the cached value. + expect(player_config.excluded?).to be_truthy + expect(RailsAdmin::AbstractModel).to have_received(:all).once + + comment_config = RailsAdmin.config(Comment) + expect(comment_config.excluded?).to be_falsey + expect(RailsAdmin::AbstractModel).to have_received(:all).twice + # Calling a second time uses the cached value. + expect(comment_config.excluded?).to be_falsey + expect(RailsAdmin::AbstractModel).to have_received(:all).twice end end @@ -79,11 +94,12 @@ i18n: { plural: { rule: ->(count) do - if count == 0 + case count + when 0 :zero - elsif count == 1 + when 1 :one - elsif count == 2 + when 2 :two else :other @@ -125,4 +141,19 @@ expect(RailsAdmin.config(Cms::BasicPage).navigation_label).to eq('Cms') end end + + describe '#last_created_at', active_record: true do + let!(:teams) do + [FactoryBot.create(:team, created_at: 1.day.ago), FactoryBot.create(:team, created_at: 2.days.ago)] + end + before do + RailsAdmin.config(Team) do + last_created_at { abstract_model.model.maximum(:created_at) } + end + end + + it 'allow customization' do + expect(RailsAdmin.config(Team).last_created_at.to_date).to eq 1.day.ago.to_date + end + end end diff --git a/spec/rails_admin/config/proxyable_spec.rb b/spec/rails_admin/config/proxyable_spec.rb index 2a0a98075b..d7598faa77 100644 --- a/spec/rails_admin/config/proxyable_spec.rb +++ b/spec/rails_admin/config/proxyable_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Proxyable do @@ -52,7 +54,7 @@ def qux it 'ensures thread-safety' do threads = Array.new(2) do |i| Thread.new do - value = %w(a b)[i] + value = %w[a b][i] proxy = proxyable_test.with foo: value sleep i * 0.1 expect(proxy.boo).to eq value diff --git a/spec/rails_admin/config/sections/list_spec.rb b/spec/rails_admin/config/sections/list_spec.rb new file mode 100644 index 0000000000..4d4f41678f --- /dev/null +++ b/spec/rails_admin/config/sections/list_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RailsAdmin::Config::Sections::List do + describe '#fields_for_table' do + subject { RailsAdmin.config(Player).list } + + it 'brings sticky fields first' do + RailsAdmin.config Player do + list do + field(:number) + field(:id) + field(:name) { sticky true } + end + end + expect(subject.fields_for_table.map(&:name)).to eq %i[name number id] + end + + it 'keep the original order except for stickey ones' do + RailsAdmin.config Player do + list do + configure(:number) { sticky true } + end + end + expect(subject.fields_for_table.map(&:name)).to eq %i[number] + (subject.visible_fields.map(&:name) - %i[number]) + end + end +end diff --git a/spec/rails_admin/config/sections_spec.rb b/spec/rails_admin/config/sections_spec.rb index ac0a64f6b1..bb3e199ca4 100644 --- a/spec/rails_admin/config/sections_spec.rb +++ b/spec/rails_admin/config/sections_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config::Sections do @@ -110,9 +112,9 @@ end end - expect(RailsAdmin.config(Team).list.visible_groups.collect { |g| g.visible_fields.collect(&:name) }).to eq([[:founded], [:name, :wins]]) - expect(RailsAdmin.config(Team).edit.visible_groups.collect { |g| g.visible_fields.collect(&:name) }).to eq([[:name], [:founded, :wins]]) - expect(RailsAdmin.config(Team).create.visible_groups.collect { |g| g.visible_fields.collect(&:name) }).to eq([[:name], [:founded, :wins]]) + expect(RailsAdmin.config(Team).list.visible_groups.collect { |g| g.visible_fields.collect(&:name) }).to eq([[:founded], %i[name wins]]) + expect(RailsAdmin.config(Team).edit.visible_groups.collect { |g| g.visible_fields.collect(&:name) }).to eq([[:name], %i[founded wins]]) + expect(RailsAdmin.config(Team).create.visible_groups.collect { |g| g.visible_fields.collect(&:name) }).to eq([[:name], %i[founded wins]]) expect(RailsAdmin.config(Team).update.visible_groups.collect { |g| g.visible_fields.collect(&:name) }).to eq([[:name], [:founded], [:wins], [:losses]]) expect(RailsAdmin.config(Team).visible_groups.collect { |g| g.visible_fields.collect(&:name) }.flatten.count).to eq(20) expect(RailsAdmin.config(Team).export.visible_groups.collect { |g| g.visible_fields.collect(&:name) }.flatten.count).to eq(20) diff --git a/spec/rails_admin/config_spec.rb b/spec/rails_admin/config_spec.rb index 3fac98cf73..910ccc1b5a 100644 --- a/spec/rails_admin/config_spec.rb +++ b/spec/rails_admin/config_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe RailsAdmin::Config do @@ -18,10 +20,6 @@ expect(RailsAdmin::AbstractModel.all.collect(&:model)).to eq([League]) end - it 'always excludes history', active_record: true do - expect(RailsAdmin::AbstractModel.all.collect(&:model)).not_to include(RailsAdmin::History) - end - it 'excluded? returns true for any model not on the list' do RailsAdmin.config.included_models = [Team, League] @@ -74,9 +72,9 @@ it 'can be configured' do RailsAdmin.config do |config| - config.main_app_name = %w(stati c value) + config.main_app_name = %w[static value] end - expect(RailsAdmin.config.main_app_name).to eq(%w(stati c value)) + expect(RailsAdmin.config.main_app_name).to eq(%w[static value]) end end @@ -134,7 +132,9 @@ class ControllerMock def set_paper_trail_whodunnit; end end + module PaperTrail; end + class Version; end RailsAdmin.add_extension(:example, RailsAdmin::Extensions::PaperTrail, auditing: true) end @@ -244,6 +244,7 @@ class RecursivelyEmbedsOne include Mongoid::Document recursively_embeds_one end + class RecursivelyEmbedsMany include Mongoid::Document recursively_embeds_many @@ -259,9 +260,25 @@ class RecursivelyEmbedsMany it 'should not include classnames start with Concerns::' do expect(RailsAdmin::Config.models_pool.select { |m| m.match(/^Concerns::/) }).to be_empty end + + it 'includes models in the directory added by config.eager_load_paths' do + expect(RailsAdmin::Config.models_pool).to include('Basketball') + end + + it 'should include a model which was configured explicitly' do + RailsAdmin::Config.model 'PaperTrail::Version' do + visible false + end + + expect(RailsAdmin::Config.models_pool).to include('PaperTrail::Version') + end end describe '.parent_controller' do + before do + class TestController < ActionController::Base; end + end + it 'uses default class' do expect(RailsAdmin.config.parent_controller).to eq '::ActionController::Base' end @@ -274,6 +291,27 @@ class RecursivelyEmbedsMany end end + describe '.parent_controller=' do + context 'if RailsAdmin::ApplicationController is already loaded' do + before do + # preload controllers (e.g. when config.eager_load = true) + RailsAdmin::MainController + end + + after do + RailsAdmin::Config.reset + RailsAdmin.send(:remove_const, :ApplicationController) + load RailsAdmin::Engine.root.join('app/controllers/rails_admin/application_controller.rb') + end + + it 'can be changed' do + RailsAdmin.config.parent_controller = 'ApplicationController' + expect(RailsAdmin::ApplicationController.superclass).to eq ApplicationController + expect(RailsAdmin::MainController.superclass.superclass).to eq ApplicationController + end + end + end + describe '.forgery_protection_settings' do it 'uses with: :exception by default' do expect(RailsAdmin.config.forgery_protection_settings).to eq(with: :exception) @@ -296,6 +334,7 @@ class RecursivelyEmbedsMany end end end + context 'when model expanded' do before do described_class.model(Team) do @@ -303,9 +342,10 @@ class RecursivelyEmbedsMany end end it 'execute all passed blocks' do - expect(fields.map(&:name)).to match_array %i(players fans) + expect(fields.map(&:name)).to match_array %i[players fans] end end + context 'when expand redefine behavior' do before do described_class.model Team do @@ -316,84 +356,61 @@ class RecursivelyEmbedsMany expect(fields.find { |f| f.name == :players }.visible).to be true end end - context 'when model expanded in config' do - let(:block) { proc { field :players } } - before do - allow(block).to receive(:source_location).and_return(['config/initializers/rails_admin.rb']) - described_class.model(Team, &block) - end - it 'executes first' do - expect(fields.find { |f| f.name == :players }.visible).to be false + + context 'when model has no table yet', active_record: true do + it 'does not try to apply the configuration block' do + described_class.model(WithoutTable) do + include_all_fields + end end end end - describe "field types code reloading" do - before { Rails.application.config.cache_classes = false } - after { Rails.application.config.cache_classes = true } - - let(:config) { described_class.model(Team) } - let(:fields) { described_class.model(Team).edit.fields } - - let(:team_config) do - proc do - field :id - field :wins, :boolean + describe '.reset' do + before do + RailsAdmin.config do |config| + config.included_models = %w[Player Team] end - end - - let(:team_config2) do - proc do - field :wins, :toggle + RailsAdmin::AbstractModel.all + RailsAdmin::Config.reset + RailsAdmin.config do |config| + config.excluded_models = ['Player'] end end + subject { RailsAdmin::AbstractModel.all.map { |am| am.model.name } } - let(:team_config3) do - proc do - field :wins - end + it 'refreshes the result of RailsAdmin::AbstractModel.all' do + expect(subject).not_to include 'Player' + expect(subject).to include 'Team' end + end - it "allows code reloading" do - Team.send(:rails_admin, &team_config) - - # This simulates the way RailsAdmin really does it - config.edit.send(:_fields, true) - - module RailsAdmin - module Config - module Fields - module Types - class Toggle < RailsAdmin::Config::Fields::Base - RailsAdmin::Config::Fields::Types.register(self) - end - end - end - end + describe '.reload!' do + before do + RailsAdmin.config Player do + field :name + end + RailsAdmin.config Team do + field :color, :integer end - Team.send(:rails_admin, &team_config2) - expect(fields.map(&:name)).to match_array %i(id wins) end - it "updates model config when reloading code for rails 5" do - Team.send(:rails_admin, &team_config) - - # this simulates rails code reloading - RailsAdmin::Engine.initializers.select do |i| - i.name == "RailsAdmin reload config in development" - end.first.block.call - Rails.application.executor.wrap do - ActiveSupport::Reloader.new.tap(&:class_unload!).complete! - end + it 'clears current configuration' do + RailsAdmin::Config.reload! + expect(RailsAdmin::Config.model(Player).fields.map(&:name)).to include :number + end - Team.send(:rails_admin, &team_config3) - expect(fields.map(&:name)).to match_array %i(wins) - end if defined?(ActiveSupport::Reloader) + it 'reloads the configuration from the initializer' do + RailsAdmin::Config.reload! + expect(RailsAdmin::Config.model(Team).fields.find { |f| f.name == :color }.type).to eq :hidden + end end end module ExampleModule class AuthorizationAdapter; end + class ConfigurationAdapter; end + class AuditingAdapter; end end diff --git a/spec/rails_admin/engine_spec.rb b/spec/rails_admin/engine_spec.rb new file mode 100644 index 0000000000..a3572662d6 --- /dev/null +++ b/spec/rails_admin/engine_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RailsAdmin::Engine do + context 'on class unload' do + let(:fields) { RailsAdmin.config(Player).edit.fields } + before do + Rails.application.config.cache_classes = false + RailsAdmin.config(Player) do + field :name + field :number + end + end + after { Rails.application.config.cache_classes = true } + + it 'triggers RailsAdmin config to be reloaded' do + # this simulates rails code reloading + RailsAdmin::Engine.initializers.find do |i| + i.name == 'RailsAdmin reload config in development' + end.block.call(Rails.application) + Rails.application.executor.wrap do + ActiveSupport::Reloader.new.tap(&:class_unload!).complete! + end + + RailsAdmin.config(Player) do + field :number + end + expect(fields.map(&:name)).to match_array %i[number] + end + end +end diff --git a/spec/rails_admin/extentions/cancancan/authorization_adapter_spec.rb b/spec/rails_admin/extentions/cancancan/authorization_adapter_spec.rb new file mode 100644 index 0000000000..f4b11635f1 --- /dev/null +++ b/spec/rails_admin/extentions/cancancan/authorization_adapter_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RailsAdmin::Extensions::CanCanCan::AuthorizationAdapter do + let(:user) { double } + let(:controller) { double(_current_user: user, current_ability: MyAbility.new(user)) } + + class MyAbility + include CanCan::Ability + def initialize(_user) + can :access, :rails_admin + can :manage, :all + end + end + + describe '#initialize' do + it 'accepts the ability class as an argument' do + expect(described_class.new(controller, MyAbility).ability_class).to eq MyAbility + end + + it 'supports block DSL' do + adapter = described_class.new(controller) do + ability_class MyAbility + end + expect(adapter.ability_class).to eq MyAbility + end + end +end diff --git a/spec/rails_admin/extentions/paper_trail/auditing_adapter_spec.rb b/spec/rails_admin/extentions/paper_trail/auditing_adapter_spec.rb index 9130116c62..67614cd179 100644 --- a/spec/rails_admin/extentions/paper_trail/auditing_adapter_spec.rb +++ b/spec/rails_admin/extentions/paper_trail/auditing_adapter_spec.rb @@ -1,21 +1,41 @@ +# frozen_string_literal: true + require 'spec_helper' -RSpec.describe RailsAdmin::Extensions::PaperTrail::VersionProxy do - describe '#username' do - subject { described_class.new(version, user_class).username } +RSpec.describe RailsAdmin::Extensions::PaperTrail::AuditingAdapter, active_record: true do + let(:controller) { double(set_paper_trail_whodunnit: nil) } - let(:version) { double(whodunnit: :the_user) } - let(:user_class) { double(find: user) } + describe '#initialize' do + it 'accepts the user and version classes as arguments' do + adapter = described_class.new(controller, User::Confirmed, Trail) + expect(adapter.user_class).to eq User::Confirmed + expect(adapter.version_class).to eq Trail + end - context 'when found user has email' do - let(:user) { double(email: :mail) } - it { is_expected.to eq(:mail) } + it 'supports block DSL' do + adapter = described_class.new(controller) do + user_class User::Confirmed + version_class Trail + sort_by(created_at: :asc) + end + expect(adapter.user_class).to eq User::Confirmed + expect(adapter.version_class).to eq Trail + expect(adapter.sort_by).to eq({created_at: :asc}) end + end - context 'when found user does not have email' do - let(:user) { double } # no email method + describe '#listing_for_model' do + subject { RailsAdmin::Extensions::PaperTrail::AuditingAdapter.new(nil) } + let(:model) { RailsAdmin::AbstractModel.new(PaperTrailTest) } + + it 'uses the given sort order' do + expect_any_instance_of(ActiveRecord::Relation).to receive(:order).with(whodunnit: :asc).and_call_original + subject.listing_for_model model, nil, :username, false, true, nil, 20 + end - it { is_expected.to eq(:the_user) } + it 'uses the default order when sort is not given' do + expect_any_instance_of(ActiveRecord::Relation).to receive(:order).with(id: :desc).and_call_original + subject.listing_for_model model, nil, false, false, true, nil, 20 end end end diff --git a/spec/rails_admin/extentions/paper_trail/version_proxy_spec.rb b/spec/rails_admin/extentions/paper_trail/version_proxy_spec.rb new file mode 100644 index 0000000000..98e95b5704 --- /dev/null +++ b/spec/rails_admin/extentions/paper_trail/version_proxy_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RailsAdmin::Extensions::PaperTrail::VersionProxy, active_record: true do + describe '#username' do + subject { described_class.new(version, user_class).username } + + let(:version) { double(whodunnit: :the_user) } + let(:user_class) { double(find: user) } + + context 'when found user has email' do + let(:user) { double(email: :mail) } + it { is_expected.to eq(:mail) } + end + + context 'when found user does not have email' do + let(:user) { double } # no email method + + it { is_expected.to eq(:the_user) } + end + end +end diff --git a/spec/rails_admin/install_generator_spec.rb b/spec/rails_admin/install_generator_spec.rb index da5684e326..1d819c0a82 100644 --- a/spec/rails_admin/install_generator_spec.rb +++ b/spec/rails_admin/install_generator_spec.rb @@ -1,28 +1,118 @@ +# frozen_string_literal: true + require 'spec_helper' require 'generators/rails_admin/install_generator' RSpec.describe RailsAdmin::InstallGenerator, type: :generator do - destination File.expand_path('../../dummy_app/tmp/generator', __FILE__) - arguments ['admin'] + destination File.expand_path('../dummy_app/tmp/generator', __dir__) + arguments ['admin', "--asset=#{CI_ASSET}", '--force'] before do prepare_destination + File.write(File.join(destination_root, 'package.json'), '{"license": "MIT"}') + FileUtils.touch File.join(destination_root, 'Gemfile') + FileUtils.mkdir_p(File.join(destination_root, 'config/initializers')) + File.write(File.join(destination_root, 'config/routes.rb'), <<~RUBY) + Rails.application.routes.draw do + # empty + end + RUBY + File.write(File.join(destination_root, 'Rakefile'), <<-RUBY.gsub(/^ {4}/, '')) + desc 'Stub for testing' + task 'css:install:sass' + RUBY + end + + after do + FileUtils.rm_rf(destination_root) end it 'mounts RailsAdmin as Engine and generates RailsAdmin Initializer' do - expect_any_instance_of(generator_class).to receive(:route). - with("mount RailsAdmin::Engine => '/admin', as: 'rails_admin'") - silence_stream(STDOUT) do - generator.invoke('install') + Dir.chdir(destination_root) do + run_generator end - expect(destination_root).to have_structure{ - directory 'config' do - directory 'initializers' do - file 'rails_admin.rb' do - contains 'RailsAdmin.config' + expect(destination_root).to( + have_structure do + directory 'config' do + directory 'initializers' do + file 'rails_admin.rb' do + contains 'RailsAdmin.config' + contains 'asset_source =' + end + end + file 'routes.rb' do + contains "mount RailsAdmin::Engine => '/admin', as: 'rails_admin'" end end + case CI_ASSET + when :webpacker + file 'app/javascript/packs/rails_admin.js' do + contains 'import "rails_admin/src/rails_admin/base"' + end + file 'app/javascript/stylesheets/rails_admin.scss' do + contains '@import "rails_admin/src/rails_admin/styles/base"' + end + when :sprockets + file 'Gemfile' do + contains 'sassc-rails' + end + when :importmap + file 'app/javascript/rails_admin.js' do + contains 'import "rails_admin/src/rails_admin/base"' + end + file 'app/assets/stylesheets/rails_admin.scss' do + contains '$fa-font-path: ".";' + contains '@import "rails_admin/src/rails_admin/styles/base"' + end + file 'config/importmap.rails_admin.rb' do + contains 'pin "rails_admin", preload: true' + contains 'pin "rails_admin/src/rails_admin/base", to: "https://ga.jspm.io/npm:rails_admin@' + contains 'pin "bootstrap", to: "https://ga.jspm.io/npm:bootstrap@' + end + file 'config/initializers/assets.rb' do + contains 'Rails.root.join("node_modules/@fortawesome/fontawesome-free/webfonts")' + end + file 'package.json' do + contains 'sass ./app/assets/stylesheets/rails_admin.scss:./app/assets/builds/rails_admin.css' + end + when :webpack + file 'app/javascript/rails_admin.js' do + contains 'import "rails_admin/src/rails_admin/base"' + end + file 'app/assets/stylesheets/rails_admin.scss' do + contains '$fa-font-path: ".";' + contains '@import "rails_admin/src/rails_admin/styles/base"' + end + file 'package.json' do + contains 'webpack --config webpack.config.js' + contains 'sass ./app/assets/stylesheets/rails_admin.scss:./app/assets/builds/rails_admin.css' + end + when :vite + file 'app/frontend/entrypoints/rails_admin.js' do + contains 'import "~/stylesheets/rails_admin.scss"' + contains 'import "rails_admin/src/rails_admin/base"' + end + file 'app/frontend/stylesheets/rails_admin.scss' do + contains '$fa-font-path: "@fortawesome/fontawesome-free/webfonts";' + contains '@import "rails_admin/src/rails_admin/styles/base"' + end + file 'package.json' do + contains 'sass' + end + end + end, + ) + end + + it 'inserts asset_source option to RailsAdmin Initializer' do + File.write(File.join(destination_root, 'config/initializers/rails_admin.rb'), <<~RUBY) + RailsAdmin.config do |config| + # empty end - } + RUBY + Dir.chdir(destination_root) do + run_generator + end + expect(File.read(File.join(destination_root, 'config/initializers/rails_admin.rb'))).to include 'config.asset_source =' end end diff --git a/spec/rails_admin/support/csv_converter_spec.rb b/spec/rails_admin/support/csv_converter_spec.rb index fbe9ab7cb6..e3141e77b0 100644 --- a/spec/rails_admin/support/csv_converter_spec.rb +++ b/spec/rails_admin/support/csv_converter_spec.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 +# frozen_string_literal: true require 'spec_helper' @@ -13,27 +13,49 @@ FactoryBot.create :player objects = Player.all - schema = {only: [:number, :name]} + schema = {only: %i[number name]} expect(RailsAdmin::CSVConverter.new(objects, schema).to_csv({})[2]).to match(/Number,Name/) end - it 'keeps headers ordering with custom method' do - RailsAdmin.config(Player) do - export do - field :number - field :number_name do - formatted_value do - bindings[:object].number_name - end + describe '#generate_csv_header' do + let(:objects) { FactoryBot.create_list :player, 1 } + before do + RailsAdmin.config(Player) do + export do + field :number + field :name end - field :name end end - FactoryBot.create :player - objects = Player.all - schema = { only: [:number, :name], methods: [:number_name] } - expect(RailsAdmin::CSVConverter.new(objects, schema).to_csv({})[2]).to match(/Number,Number name,Name/) + it 'does not break when non-existent fields are given' do + expect(RailsAdmin::CSVConverter.new(objects, {only: %i[name foo], include: {bar: :baz}}).send(:generate_csv_header)). + to eq ['Name'] + end + + it 'does not break when non-association fields are given to :include' do + expect(RailsAdmin::CSVConverter.new(objects, {only: %i[name foo], include: {name: :name}}).send(:generate_csv_header)). + to eq ['Name'] + end + + it 'keeps headers ordering with custom method' do + RailsAdmin.config(Player) do + export do + field :number + field :number_name do + formatted_value do + bindings[:object].number_name + end + end + field :name + end + end + + FactoryBot.create :player + objects = Player.all + schema = { only: [:number, :name], methods: [:number_name] } + expect(RailsAdmin::CSVConverter.new(objects, schema).to_csv({})[2]).to match(/Number,Number name,Name/) + end end describe '#to_csv' do @@ -47,7 +69,7 @@ end let(:objects) { FactoryBot.create_list :player, 1, number: 1, name: 'なまえ' } - let(:schema) { {only: [:number, :name]} } + let(:schema) { {only: %i[number name]} } let(:options) { {encoding_to: encoding} } subject { RailsAdmin::CSVConverter.new(objects, schema).to_csv(options) } @@ -65,7 +87,7 @@ before do case connection_config[:adapter] when 'postgresql' - @connection = ActiveRecord::Base.connection.instance_variable_get(:@connection) + @connection = ActiveRecord::Base.connection.raw_connection @connection.set_client_encoding('latin1') when 'mysql2' ActiveRecord::Base.connection.execute('SET NAMES latin1;') @@ -83,7 +105,7 @@ it 'exports to ISO-8859-1' do expect(subject[1]).to eq 'ISO-8859-1' expect(subject[2].encoding).to eq Encoding::ISO_8859_1 - expect(subject[2].unpack('H*').first). + expect(subject[2].unpack1('H*')). to eq '4e756d6265722c4e616d650a312c4a6f73e80a' end end @@ -94,7 +116,7 @@ it 'exports to UTF-8 with BOM' do expect(subject[1]).to eq 'UTF-8' expect(subject[2].encoding).to eq Encoding::UTF_8 - expect(subject[2].unpack('H*').first). + expect(subject[2].unpack1('H*')). to eq 'efbbbf4e756d6265722c4e616d650a312ce381aae381bee381880a' # have BOM end end @@ -105,7 +127,7 @@ it 'exports to Shift_JIS' do expect(subject[1]).to eq 'Shift_JIS' expect(subject[2].encoding).to eq Encoding::Shift_JIS - expect(subject[2].unpack('H*').first). + expect(subject[2].unpack1('H*')). to eq '4e756d6265722c4e616d650a312c82c882dc82a60a' end end @@ -116,34 +138,34 @@ it 'encodes to expected byte sequence' do expect(subject[1]).to eq 'UTF-16' expect(subject[2].encoding).to eq Encoding::UTF_16 - expect(subject[2].unpack('H*').first.force_encoding('US-ASCII')). + expect(subject[2].unpack1('H*').force_encoding('US-ASCII')). to eq 'feff004e0075006d006200650072002c004e0061006d0065000a0031002c306a307e3048000a' end end - context "when specifying a column separator" do - context "when options keys are symbolized" do + context 'when specifying a column separator' do + context 'when options keys are symbolized' do let(:options) { {encoding_to: 'UTF-8', generator: {col_sep: '___'}} } - it "uses the column separator specified" do - expect(subject[2].unpack('H*').first). + it 'uses the column separator specified' do + expect(subject[2].unpack1('H*')). to eq 'efbbbf4e756d6265725f5f5f4e616d650a315f5f5fe381aae381bee381880a' end end - context "when options keys are string" do + context 'when options keys are string' do let(:options) { {'encoding_to' => 'UTF-8', 'generator' => {'col_sep' => '___'}} } - it "uses the column separator specified" do - expect(subject[2].unpack('H*').first). + it 'uses the column separator specified' do + expect(subject[2].unpack1('H*')). to eq 'efbbbf4e756d6265725f5f5f4e616d650a315f5f5fe381aae381bee381880a' end end end - context "when objects is empty" do + context 'when objects is empty' do let(:objects) { [] } let(:options) { {} } - it "generates an empty csv" do + it 'generates an empty csv' do expect(subject[2]).to eq("\n") end end diff --git a/spec/rails_admin/support/datetime_spec.rb b/spec/rails_admin/support/datetime_spec.rb index cc404d74f3..27bfe757e5 100644 --- a/spec/rails_admin/support/datetime_spec.rb +++ b/spec/rails_admin/support/datetime_spec.rb @@ -1,19 +1,29 @@ -# encoding: utf-8 +# frozen_string_literal: true require 'spec_helper' RSpec.describe RailsAdmin::Support::Datetime do - describe '#to_momentjs' do + describe '#to_flatpickr_format' do { - '%D de %M de %Y, %H:%M:%S' => 'MM/DD/YY [de] mm [de] YYYY, HH:mm:ss', - '%d/%-m/%Y, %H:%M:%S' => 'DD/M/YYYY, HH:mm:ss', - '%d de %B de %Y' => 'DD [de] MMMM [de] YYYY', - '%-d %B %Y' => 'D MMMM YYYY', - }.each do |strftime_format, momentjs_format| - it "convert strftime_format to momentjs_format - example #{strftime_format}" do - strftime_format = RailsAdmin::Support::Datetime.new(strftime_format) - expect(strftime_format.to_momentjs).to eq momentjs_format + '%D de %M de %Y, %H:%M:%S' => 'm/d/y \d\e i \d\e Y, H:i:S', + '%d/%-m/%Y, %H:%M:%S' => 'd/n/Y, H:i:S', + '%d de %B de %Y' => 'd \d\e F \d\e Y', + '%-d %B %Y' => 'j F Y', + '%F %T' => 'Y-m-d H:i:S', + '%Y-%m-%dT%H:%M:%S%:z' => 'Y-m-d\TH:i:S+00:00', + '%HH%MM%SS' => 'H\Hi\MS\S', + 'a%-Ha%-Ma%-Sa%:za' => '\aH\ai\as\a+00:00\a', + '%B %-d at %-l:%M %p' => 'F j \a\t h:i K', + }.each do |strftime_format, flatpickr_format| + it "convert strftime_format to flatpickr_format - example #{strftime_format}" do + expect(RailsAdmin::Support::Datetime.to_flatpickr_format(strftime_format)).to eq flatpickr_format end end + + it 'raises an error with unsupported directive' do + expect do + RailsAdmin::Support::Datetime.to_flatpickr_format('%C') + end.to raise_error(/Unsupported/) + end end end diff --git a/spec/rails_admin/support/hash_helper_spec.rb b/spec/rails_admin/support/hash_helper_spec.rb index 83744f7171..52e52c7d10 100644 --- a/spec/rails_admin/support/hash_helper_spec.rb +++ b/spec/rails_admin/support/hash_helper_spec.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 +# frozen_string_literal: true require 'spec_helper' @@ -21,13 +21,13 @@ let(:symbolized_hash) { RailsAdmin::HashHelper.symbolize(hash) } it 'symbolizes top-level hash keys' do - [:subject, :user].each do |key| + %i[subject user].each do |key| expect(symbolized_hash.keys).to include(key) end end it 'symbolizes nested hashes' do - [:name, :title, :clients].each do |key| + %i[name title clients].each do |key| expect(symbolized_hash[:user].keys).to include(key) end end diff --git a/spec/rails_admin/version_spec.rb b/spec/rails_admin/version_spec.rb new file mode 100644 index 0000000000..14beeb3d09 --- /dev/null +++ b/spec/rails_admin/version_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'spec_helper' + +RSpec.describe 'RailsAdmin::Version' do + describe '#warn_with_js_version' do + it 'does nothing when the versions match' do + allow(RailsAdmin::Version).to receive(:actual_js_version).and_return('3.0.0') + allow(RailsAdmin::Version).to receive(:js).and_return('3.0.0') + expect(RailsAdmin::Version).not_to receive(:warn) + RailsAdmin::Version.warn_with_js_version + end + + it "shows a warning when actual_js_version couldn't detected" do + allow(RailsAdmin::Version).to receive(:actual_js_version).and_return(nil) + allow(RailsAdmin::Version).to receive(:js).and_return('3.0.1') + expect(RailsAdmin::Version).to receive(:warn).with(/yarn install/) + RailsAdmin::Version.warn_with_js_version + end + + it 'shows a warning when the versions do not match' do + allow(RailsAdmin::Version).to receive(:actual_js_version).and_return('3.0.0') + allow(RailsAdmin::Version).to receive(:js).and_return('3.0.1') + expect(RailsAdmin::Version).to receive(:warn).with(/inconsistency/) + RailsAdmin::Version.warn_with_js_version + end + end + + describe '#js_version_from_node_modules' do + unless CI_ASSET == :sprockets + let(:path) { Rails.root.join('node_modules/rails_admin/package.json') } + before do + @backup = File.read(path) + FileUtils.rm(path) + end + after { File.write(path, @backup) } + + it 'returns nil when RailsAdmin package.json is not found' do + expect(RailsAdmin::Version.send(:js_version_from_node_modules)).to be_nil + end + + it 'shows a warning when RailsAdmin package.json is not found' do + File.write(path, '{"version": "0.1.0-alpha"}') + expect(RailsAdmin::Version.send(:js_version_from_node_modules)).to eq '0.1.0-alpha' + end + end + end +end diff --git a/spec/shared_examples/shared_examples_for_field_types.rb b/spec/shared_examples/shared_examples_for_field_types.rb index 2dec54b547..acfd525240 100644 --- a/spec/shared_examples/shared_examples_for_field_types.rb +++ b/spec/shared_examples/shared_examples_for_field_types.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.shared_examples 'a generic field type' do |column_name, field_type| @@ -34,3 +36,41 @@ expect(subject).to be_a(RailsAdmin::Config::Fields::Types::StringLike) end end + +RSpec.shared_examples 'a float-like field type' do |column_name| + subject do + RailsAdmin.config('FieldTest').fields.detect do |f| + f.name == column_name + end.with(object: FieldTest.new) + end + + describe '#html_attributes' do + it 'should contain a step attribute' do + expect(subject.html_attributes[:step]).to eq('any') + end + + it 'should contain a falsey required attribute' do + expect(subject.html_attributes[:required]).to be_falsey + end + + context 'when the field is required' do + before do + RailsAdmin.config FieldTest do + field column_name, :float do + required true + end + end + end + + it 'should contain a truthy required attribute' do + expect(subject.html_attributes[:required]).to be_truthy + end + end + end + + describe '#view_helper' do + it "uses the 'number' type input tag" do + expect(subject.view_helper).to eq(:number_field) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 63e4da7ab8..d6280259e7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,9 @@ -# Configure Rails Envinronment +# frozen_string_literal: true + +# Configure Rails Environment ENV['RAILS_ENV'] = 'test' CI_ORM = (ENV['CI_ORM'] || :active_record).to_sym -CI_TARGET_ORMS = [:active_record, :mongoid].freeze +CI_TARGET_ORMS = %i[active_record mongoid].freeze PK_COLUMN = {active_record: :id, mongoid: :_id}[CI_ORM] if RUBY_ENGINE == 'jruby' @@ -18,7 +20,6 @@ SimpleCov.start do add_filter '/spec/' add_filter '/vendor/bundle/' - minimum_coverage(CI_ORM == :mongoid ? 90.05 : 91.21) end SimpleCov::Formatter::LcovFormatter.config do |c| @@ -26,17 +27,18 @@ c.single_report_path = 'coverage/lcov.info' end -require File.expand_path('../dummy_app/config/environment', __FILE__) +require File.expand_path('dummy_app/config/environment', __dir__) require 'rspec/rails' require 'factory_bot' require 'factories' require 'policies' -require 'database_cleaner' +require "database_cleaner/#{CI_ORM}" require "orm/#{CI_ORM}" +require 'paper_trail/frameworks/rspec' if defined?(PaperTrail) -Dir[File.expand_path('../support/**/*.rb', __FILE__), - File.expand_path('../shared_examples/**/*.rb', __FILE__)].each { |f| require f } +Dir[File.expand_path('support/**/*.rb', __dir__), + File.expand_path('shared_examples/**/*.rb', __dir__)].sort.each { |f| require f } ActionMailer::Base.delivery_method = :test ActionMailer::Base.perform_deliveries = true @@ -44,27 +46,11 @@ Rails.backtrace_cleaner.remove_silencers! -# Don't need passwords in test DB to be secure, but we would like 'em to be -# fast -- and the stretches mechanism is intended to make passwords -# computationally expensive. -module Devise - module Models - module DatabaseAuthenticatable - protected - - def password_digest(password) - password - end - end - end -end - -Devise.setup do |config| - config.stretches = 0 +require 'capybara/cuprite' +Capybara.javascript_driver = :cuprite +Capybara.register_driver(:cuprite) do |app| + Capybara::Cuprite::Driver.new(app, js_errors: true, logger: ConsoleLogger) end - -require 'capybara/poltergeist' -Capybara.javascript_driver = :poltergeist Capybara.server = :webrick RailsAdmin.setup_all_extensions @@ -82,23 +68,48 @@ def password_digest(password) config.include Warden::Test::Helpers config.include Capybara::DSL, type: :request + config.include Capybara::RSpecMatchers, type: :request config.verbose_retry = true config.display_try_failure_messages = true config.around :each, :js do |example| - example.run_with_retry retry: 2 + example.run_with_retry retry: (ENV['CI'] && RUBY_ENGINE == 'jruby' ? 3 : 2) end config.retry_callback = proc do |example| - Capybara.reset! if example.metadata[:js] + example.metadata[:retry] = 6 if [Ferrum::DeadBrowserError, Ferrum::NoExecutionContextError, Ferrum::TimeoutError].include?(example.exception.class) + if example.metadata[:js] + attempt = 0 + begin + Capybara.reset! + rescue Ferrum::TimeoutError, Ferrum::NoExecutionContextError + attempt += 1 + raise if attempt >= 5 + + retry + end + end + end + + config.before(:all) do + case CI_ASSET + when :webpacker + Webpacker.instance.compiler.compile + when :vite + ViteRuby.instance.commands.build + end end config.before do |example| - DatabaseCleaner.strategy = (CI_ORM == :mongoid || example.metadata[:js]) ? :truncation : :transaction + DatabaseCleaner.strategy = + if CI_ORM == :mongoid || example.metadata[:js] + :deletion + else + :transaction + end DatabaseCleaner.start RailsAdmin::Config.reset - RailsAdmin::AbstractModel.reset - RailsAdmin::Config.audit_with(:history) if CI_ORM == :active_record + RailsAdmin::Config.asset_source = CI_ASSET end config.after(:each) do @@ -108,9 +119,11 @@ def password_digest(password) CI_TARGET_ORMS.each do |orm| if orm == CI_ORM - config.filter_run_excluding "skip_#{orm}".to_sym => true + config.filter_run_excluding "skip_#{orm}": true else config.filter_run_excluding orm => true end end + + config.filter_run_excluding composite_primary_keys: true unless defined?(ActiveRecord) && ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) end diff --git a/spec/support/cuprite_logger.rb b/spec/support/cuprite_logger.rb new file mode 100644 index 0000000000..1291a3a3fd --- /dev/null +++ b/spec/support/cuprite_logger.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ConsoleLogger + def self.puts(message) + warn(message) unless message.start_with?(' ◀', "\n\n▶") + end +end diff --git a/spec/support/fakeio.rb b/spec/support/fakeio.rb index 82a75300ce..3bab187095 100644 --- a/spec/support/fakeio.rb +++ b/spec/support/fakeio.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'forwardable' require 'stringio' @@ -11,5 +13,5 @@ def initialize(content, filename: nil, content_type: nil) end extend Forwardable - delegate %i(read rewind eof? close size) => :@io + delegate %i[read rewind eof? close size] => :@io end diff --git a/spec/support/fixtures.rb b/spec/support/fixtures.rb index e00bc8a224..6713ca263f 100644 --- a/spec/support/fixtures.rb +++ b/spec/support/fixtures.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + def file_path(*paths) File.expand_path(File.join(File.dirname(__FILE__), '../fixtures', *paths)) end diff --git a/src/rails_admin/abstract-select.js b/src/rails_admin/abstract-select.js new file mode 100644 index 0000000000..161157a615 --- /dev/null +++ b/src/rails_admin/abstract-select.js @@ -0,0 +1,30 @@ +import jQuery from "jquery"; +import "jquery-ui/ui/widget.js"; + +(function ($) { + "use strict"; + + $.widget("ra.abstractSelect", { + options: { + createQuery: function (query) { + if ($.isEmptyObject(this.scopeBy)) { + return { query: query }; + } else { + const filterQuery = {}; + for (var field in this.scopeBy) { + const targetField = this.scopeBy[field]; + const targetValue = $(`[name$="[${field}]"]`).val(); + if (!filterQuery[targetField]) { + filterQuery[targetField] = []; + } + filterQuery[targetField].push( + targetValue ? { o: "is", v: targetValue } : { o: "_blank" } + ); + } + return { query: query, f: filterQuery }; + } + }, + scopeBy: {}, + }, + }); +})(jQuery); diff --git a/src/rails_admin/base.js b/src/rails_admin/base.js new file mode 100644 index 0000000000..8e2685bab5 --- /dev/null +++ b/src/rails_admin/base.js @@ -0,0 +1,32 @@ +import Rails from "@rails/ujs"; +import "@hotwired/turbo-rails"; +import "./jquery"; +import "./vendor/jquery_nested_form"; +import "bootstrap"; + +// These jQuery-UI indirect dependencies need to be preloaded to be used within Import maps +import "jquery-ui/ui/version.js"; +import "jquery-ui/ui/keycode.js"; +import "jquery-ui/ui/position.js"; +import "jquery-ui/ui/safe-active-element.js"; +import "jquery-ui/ui/data.js"; +import "jquery-ui/ui/ie.js"; +import "jquery-ui/ui/scroll-parent.js"; +import "jquery-ui/ui/unique-id.js"; +import "jquery-ui/ui/widget.js"; +import "jquery-ui/ui/widgets/menu.js"; +import "jquery-ui/ui/widgets/mouse.js"; + +import "./abstract-select"; +import "./filter-box"; +import "./filtering-multiselect"; +import "./filtering-select"; +import "./nested-form-hooks"; +import "./remote-form"; +import "./sidescroll"; +import "./ui"; +import "./widgets"; + +if (!window._rails_loaded) { + Rails.start(); +} diff --git a/src/rails_admin/filter-box.js b/src/rails_admin/filter-box.js new file mode 100644 index 0000000000..45a8c9558c --- /dev/null +++ b/src/rails_admin/filter-box.js @@ -0,0 +1,360 @@ +import jQuery from "jquery"; +import I18n from "./i18n"; +import flatpickr from "flatpickr"; + +(function ($) { + var filters; + + $.filters = filters = { + append: function (options) { + var field_label = options["label"]; + var field_name = options["name"]; + var field_type = options["type"]; + var field_value = options["value"] || ""; + var field_operator = options["operator"]; + var operators = options["operators"]; + var index = options["index"]; + var value_name = "f[" + field_name + "][" + index + "][v]"; + var operator_name = "f[" + field_name + "][" + index + "][o]"; + var control = null; + var additional_control = null; + + if (operators.length > 0) { + control = $( + '' + ).prop("name", operator_name); + + operators.forEach((operator) => { + var element = this.build_operator(operator, options); + if (!element) { + return; + } + if (element.prop("value") === field_operator) { + element.prop("selected", true); + } + control.append(element); + }); + + if (control.find("[data-additional-fieldset]").length > 0) { + control.addClass("switch-additional-fieldsets"); + } + } + + switch (field_type) { + case "boolean": + control && + control + .prop("name", value_name) + .find("option") + .each(function () { + if ($(this).attr("value") === field_value) + $(this).attr("selected", true); + }); + break; + case "date": + case "datetime": + case "timestamp": + case "time": + additional_control = $.map( + [undefined, "-∞", "∞"], + function (placeholder, index) { + var visible = + index == 0 + ? !field_operator || field_operator == "default" + : field_operator == "between"; + return $('') + .addClass(index == 0 ? "default" : "between") + .css("display", visible ? "inline-block" : "none") + .html( + $( + '' + ) + .addClass(field_type == "date" ? "date" : "datetime") + .prop("name", value_name + "[]") + .prop("value", field_value[index] || "") + .prop( + "size", + field_type == "date" || field_type == "time" ? 20 : 25 + ) + .prop("placeholder", placeholder) + ); + } + ); + break; + case "enum": + if (control) { + var multiple = field_value instanceof Array; + control = control + .prop("name", multiple ? value_name + "[]" : value_name) + .prop("multiple", multiple) + .add( + $('').append( + $("").addClass( + "fas fa-" + (multiple ? "minus" : "plus") + ) + ) + ); + control.find("option").each(function () { + const value = $(this).attr("value"); + if ( + multiple ? field_value.includes(value) : value === field_value + ) + $(this).attr("selected", true); + }); + if (multiple) + control.find("option[value^=_],option[disabled]").hide(); + } + break; + case "citext": + case "string": + case "text": + case "belongs_to_association": + case "has_one_association": + additional_control = $( + '' + ) + .css( + "display", + field_operator == "_present" || field_operator == "_blank" + ? "none" + : "inline-block" + ) + .prop("name", value_name) + .prop("value", field_value); + break; + case "integer": + case "decimal": + case "float": + additional_control = $( + '' + ) + .css( + "display", + !field_operator || field_operator == "default" + ? "inline-block" + : "none" + ) + .prop("type", field_type) + .prop("name", value_name + "[]") + .prop("value", field_value[0] || "") + .add( + $( + '' + ) + .css( + "display", + field_operator == "between" ? "inline-block" : "none" + ) + .prop("type", field_type) + .prop("name", value_name + "[]") + .prop("value", field_value[1] || "") + ) + .add( + $( + '' + ) + .css( + "display", + field_operator == "between" ? "inline-block" : "none" + ) + .prop("type", field_type) + .prop("name", value_name + "[]") + .prop("value", field_value[2] || "") + ); + break; + default: + control = $( + '' + ) + .prop("name", value_name) + .prop("value", field_value); + break; + } + + var filterContainerId = field_name + "-" + index + "-filter-container"; + $("#" + filterContainerId).remove(); + + var $content = $("
    ") + .attr("id", filterContainerId) + .addClass("filter d-inline-block my-1") + .append( + $( + '' + ) + .append('') + .append(document.createTextNode(field_label)) + ) + .append(" ") + .append(control) + .append(" ") + .append(additional_control); + + $("#filters_box").append($content); + + $content.find(".date, .datetime").each(function () { + flatpickr( + this, + $.extend( + { + dateFormat: "Y-m-dTH:i:S", + altInput: true, + locale: I18n.locale, + }, + options["datetimepicker_options"] + ) + ); + }); + + $("hr.filters_box:hidden").show("slow"); + }, + build_operator: function (operator, options) { + if (operator instanceof Object) { + var element = $(""); + element.text(operator.label); + delete operator.label; + for (const key in operator) { + element.attr(key, operator[key]); + } + return element; + } + switch (operator) { + case "_discard": + return $(''); + case "_separator": + return $(''); + case "_present": + return $('').text( + I18n.t("is_present") + ); + case "_blank": + return $('').text(I18n.t("is_blank")); + case "_not_null": + return $('').text( + I18n.t("is_present") + ); + case "_null": + return $('').text(I18n.t("is_blank")); + + case "true": + return $('').text(I18n.t("true")); + case "false": + return $('').text(I18n.t("false")); + + case "today": + return $('').text(I18n.t("today")); + case "yesterday": + return $('').text( + I18n.t("yesterday") + ); + case "this_week": + return $('').text( + I18n.t("this_week") + ); + case "last_week": + return $('').text( + I18n.t("last_week") + ); + + case "like": + return $( + '' + ).text(I18n.t("contains")); + case "not_like": + return $( + '' + ).text(I18n.t("does_not_contain")); + case "is": + return $( + '' + ).text(I18n.t("is_exactly")); + case "starts_with": + return $( + '' + ).text(I18n.t("starts_with")); + case "ends_with": + return $( + '' + ).text(I18n.t("ends_with")); + + case "default": + var label; + switch (options.type) { + case "date": + case "datetime": + case "timestamp": + label = I18n.t("date"); + break; + case "time": + label = I18n.t("time"); + break; + case "integer": + case "decimal": + case "float": + label = I18n.t("number"); + break; + } + return $( + '' + ).text(label); + case "between": + return $( + '' + ).text(I18n.t("between_and_")); + + default: + return null; + } + }, + }; + + $(document).on("click", "#filters a", function (e) { + e.preventDefault(); + $.filters.append( + $.extend( + { index: $.now().toString().slice(6, 11) }, + $(this).data("options") + ) + ); + }); + + $(document).on("click", "#filters_box .delete", function (e) { + e.preventDefault(); + var form = $(this).parents("form"); + $(this).parents(".filter").remove(); + !$("#filters_box").children().length && + $("hr.filters_box:visible").hide("slow"); + }); + + $(document).on("click", "#filters_box .switch-select", function (e) { + e.preventDefault(); + var select = $(this).siblings("select"); + select.attr("multiple", !select.attr("multiple")); + select.attr( + "name", + select.attr("multiple") + ? select.attr("name") + "[]" + : select.attr("name").replace(/\[\]$/, "") + ); + select.find("option[value^=_],option[disabled]").toggle(); + $(this).find("i").toggleClass("fa-plus fa-minus"); + }); + + $(document).on( + "change", + "#filters_box .switch-additional-fieldsets", + function (e) { + var selected_option = $(this).find("option:selected"); + var klass = $(selected_option).data("additional-fieldset"); + if (klass) { + $(this) + .siblings(".additional-fieldset:not(." + klass + ")") + .hide("slow"); + $(this) + .siblings("." + klass) + .show("slow"); + } else { + $(this).siblings(".additional-fieldset").hide("slow"); + } + } + ); +})(jQuery); diff --git a/src/rails_admin/filtering-multiselect.js b/src/rails_admin/filtering-multiselect.js new file mode 100644 index 0000000000..871bdfa5a3 --- /dev/null +++ b/src/rails_admin/filtering-multiselect.js @@ -0,0 +1,387 @@ +import jQuery from "jquery"; +import "jquery-ui/ui/widget.js"; +import I18n from "./i18n"; + +(function ($) { + $.widget("ra.filteringMultiselect", $.ra.abstractSelect, { + _cache: {}, + options: { + sortable: false, + removable: true, + regional: { + add: "Add", + chooseAll: "Choose all", + clearAll: "Clear all", + down: "Down", + remove: "Remove", + search: "Search", + up: "Up", + }, + searchDelay: 400, + remote_source: null, + xhr: false, + }, + + wrapper: null, + filter: null, + collection: null, + addAll: null, + add: null, + remove: null, + up: null, + down: null, + selection: null, + removeAll: null, + + _create: function () { + this._cache = {}; + this._build(); + this._buildCache(); + this._bindEvents(); + }, + + _build: function () { + var i; + + this.wrapper = this.element.siblings( + '.ra-multiselect[data-input-for="' + this.element.attr("id") + '"]' + ); + + // Prevent duplication on browser back + if (this.wrapper.length > 0) { + this.filter = this.wrapper.find("input.ra-multiselect-search"); + this.collection = this.wrapper.find("select.ra-multiselect-collection"); + this.addAll = this.wrapper.find("a.ra-multiselect-item-add-all"); + this.add = this.wrapper.find("a.ra-multiselect-item-add"); + this.remove = this.wrapper.find("a.ra-multiselect-item-remove"); + this.up = this.wrapper.find("a.ra-multiselect-item-up"); + this.down = this.wrapper.find("a.ra-multiselect-item-down"); + this.selection = this.wrapper.find("select.ra-multiselect-selection"); + this.removeAll = this.wrapper.find("a.ra-multiselect-item-remove-all"); + return; + } + + this.wrapper = $('
    ').attr( + "data-input-for", + this.element.attr("id") + ); + this.wrapper.insertAfter(this.element); + var header = $('
    '); + this.filter = $( + '' + ); + header.append(this.filter); + this.wrapper.append(header); + + var columns = { + left: $('
    '), + center: $('
    '), + right: $('
    '), + }; + + for (i in columns) { + if (columns.hasOwnProperty(i)) { + this.wrapper.append(columns[i]); + } + } + + this.collection = $(''); + this.collection.addClass("form-control ra-multiselect-collection"); + this.addAll = $( + '' + + this.options.regional.chooseAll + + "" + ); + columns.left.html(this.collection).append(this.addAll); + this.collection.wrap('
    '); + + this.add = $( + '' + ).attr("title", this.options.regional.add); + columns.center.append(this.add); + if (this.options.removable) { + this.remove = $( + '' + ).attr("title", this.options.regional.remove); + columns.center.append(this.remove); + } + if (this.options.sortable) { + this.up = $( + '' + ).attr("title", this.options.regional.up); + this.down = $( + '' + ).attr("title", this.options.regional.down); + columns.center.append(this.up).append(this.down); + } + + this.selection = $( + '' + ); + columns.right.append(this.selection); + if (this.options.removable) { + this.removeAll = $( + '' + + this.options.regional.clearAll + + "" + ); + columns.right.append(this.removeAll); + } + this.selection.wrap('
    '); + + this.element.css({ display: "none" }); + + this.tooManyObjectsPlaceholder = $('") + .prop("value", matches[filtered[i]].id) + .prop("title", matches[filtered[i]].label) + .text(matches[filtered[i]].label); + widget.collection.append(newOptions); + } + } else { + widget.collection.html(widget.noObjectsPlaceholder); + } + }); + }, + + /* + * Cache key is stored in the format `o_") + .prop("value", option.value) + .prop("selected", true) + ); + } + }); + $(options).appendTo(this.selection).prop("selected", false); + }, + + _move: function (direction, options) { + var widget = this; + if (direction == "up") { + options.each(function (i, option) { + var prev = $(option).prev(); + if (prev.length > 0) { + var el = widget.element.find( + 'option[value="' + option.value + '"]' + ); + var el_prev = widget.element.find( + 'option[value="' + prev[0].value + '"]' + ); + el_prev.before(el); + prev.before($(option)); + } + }); + } else { + $.fn.reverse = [].reverse; // needed to lower last items first + options.reverse().each(function (i, option) { + var next = $(option).next(); + if (next.length > 0) { + var el = widget.element.find( + 'option[value="' + option.value + '"]' + ); + var el_next = widget.element.find( + 'option[value="' + next[0].value + '"]' + ); + el_next.after(el); + next.after($(option)); + } + }); + } + }, + + selected: function (value) { + if ( + this.selection[0].querySelectorAll('option[value="' + value + '"]')[0] + ) { + return true; + } + }, + + destroy: function () { + this.wrapper.remove(); + this.element.css({ display: "inline" }); + $.Widget.prototype.destroy.apply(this, arguments); + }, + }); +})(jQuery); diff --git a/src/rails_admin/filtering-select.js b/src/rails_admin/filtering-select.js new file mode 100644 index 0000000000..7695e545f3 --- /dev/null +++ b/src/rails_admin/filtering-select.js @@ -0,0 +1,306 @@ +import jQuery from "jquery"; +import "jquery-ui/ui/widget.js"; +import "jquery-ui/ui/widgets/autocomplete.js"; +import I18n from "./i18n"; + +(function ($) { + "use strict"; + + $.widget("ra.filteringSelect", $.ra.abstractSelect, { + options: { + minLength: 0, + searchDelay: 200, + remote_source: null, + source: null, + xhr: false, + }, + + button: null, + input: null, + select: null, + filtering_select: null, + + _create: function () { + this.filtering_select = this.element.siblings( + '[data-input-for="' + this.element.attr("id") + '"]' + ); + + // When using the browser back and forward buttons, it is possible that + // the autocomplete field will be cached which causes duplicate fields + // to be generated. + if (this.filtering_select.length > 0) { + this.input = this.filtering_select.children("input"); + this.button = this.filtering_select.children(".input-group-btn"); + } else { + this.element.hide(); + this.filtering_select = this._inputGroup(this.element.attr("id")); + this.input = this._inputField(); + this.button = this._buttonField(); + } + this.clearOption = $('').append( + ' ' + + $("").text(I18n.t("clear")).html() + ); + this.noObjectsPlaceholder = $('