active_fields 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +15 -2
- data/Appraisals +19 -0
- data/CHANGELOG.md +10 -0
- data/README.md +113 -12
- data/Rakefile +0 -2
- data/app/models/concerns/active_fields/customizable_concern.rb +59 -13
- data/app/models/concerns/active_fields/field_concern.rb +38 -6
- data/app/models/concerns/active_fields/value_concern.rb +8 -3
- data/db/migrate/20250229230000_add_scope_to_active_fields.rb +14 -0
- data/gemfiles/rails_7.1.gemfile +15 -0
- data/gemfiles/rails_7.2.gemfile +15 -0
- data/gemfiles/rails_8.0.gemfile +15 -0
- data/gemfiles/rails_8.1.gemfile +15 -0
- data/lib/active_fields/casters/date_array_caster.rb +2 -2
- data/lib/active_fields/casters/date_time_array_caster.rb +2 -2
- data/lib/active_fields/casters/decimal_array_caster.rb +2 -2
- data/lib/active_fields/casters/enum_array_caster.rb +2 -2
- data/lib/active_fields/casters/integer_array_caster.rb +2 -2
- data/lib/active_fields/casters/text_array_caster.rb +2 -2
- data/lib/active_fields/config.rb +2 -2
- data/lib/active_fields/finders/array_finder.rb +8 -2
- data/lib/active_fields/finders/base_finder.rb +1 -1
- data/lib/active_fields/has_active_fields.rb +3 -1
- data/lib/active_fields/version.rb +1 -1
- data/lib/generators/active_fields/scaffold/templates/controllers/active_fields_controller.rb +18 -18
- data/lib/generators/active_fields/scaffold/templates/javascript/controllers/active_field_form_controller.js +19 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_boolean.html.erb +22 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date.html.erb +22 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date_array.html.erb +22 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime.html.erb +22 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime_array.html.erb +22 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal.html.erb +22 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal_array.html.erb +22 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum.html.erb +22 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum_array.html.erb +22 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer.html.erb +22 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer_array.html.erb +22 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text.html.erb +22 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text_array.html.erb +22 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/index.html.erb +2 -0
- metadata +24 -4
- data/CODE_OF_CONDUCT.md +0 -84
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6eaf6bd2afd182c6429e472d3f2b1a3dd70bd7a7baadd9a2bbc7dc421a39a7d7
|
|
4
|
+
data.tar.gz: 6d77769c25f2c97bae25bee0ed66792fd719f4738610fa9b6f1c8d92f77f673a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 78359ebaf1b2048564eed04a8de2464aa4650d3e16a03c9f701041daaae96c7c30924fd685d15aed7180dfccf340e86509db1a0bf08dcebd1fd72ee763b1d9a9
|
|
7
|
+
data.tar.gz: 51680b929b2de155eb75e16efa328ff76e28dc0112f315ce350c378cc6fe4f77e7fee040c58a23626af7026b0afc008beac6e0a16be8892cdb89c192eaed0a5b
|
data/.rubocop.yml
CHANGED
|
@@ -3,17 +3,24 @@ plugins:
|
|
|
3
3
|
- rubocop-rails
|
|
4
4
|
- rubocop-rake
|
|
5
5
|
- rubocop-rspec
|
|
6
|
+
- rubocop-factory_bot
|
|
7
|
+
- rubocop-rspec_rails
|
|
6
8
|
|
|
7
9
|
inherit_gem:
|
|
8
10
|
rubocop-shopify: rubocop.yml
|
|
9
11
|
|
|
10
12
|
AllCops:
|
|
11
|
-
TargetRubyVersion: 3.
|
|
12
|
-
TargetRailsVersion: 8.
|
|
13
|
+
TargetRubyVersion: 3.4
|
|
14
|
+
TargetRailsVersion: 8.1
|
|
13
15
|
NewCops: enable
|
|
14
16
|
Exclude:
|
|
17
|
+
- "gemfiles/*"
|
|
15
18
|
- "spec/dummy/db/schema.rb"
|
|
16
19
|
|
|
20
|
+
Layout/LineLength:
|
|
21
|
+
Enabled: true
|
|
22
|
+
Max: 120
|
|
23
|
+
|
|
17
24
|
Layout/EmptyLinesAroundAccessModifier:
|
|
18
25
|
EnforcedStyle: around
|
|
19
26
|
|
|
@@ -47,3 +54,9 @@ RSpec/ContextWording:
|
|
|
47
54
|
|
|
48
55
|
RSpec/MultipleExpectations:
|
|
49
56
|
Enabled: false
|
|
57
|
+
|
|
58
|
+
Naming/PredicateMethod:
|
|
59
|
+
Enabled: false
|
|
60
|
+
|
|
61
|
+
Naming/InclusiveLanguage:
|
|
62
|
+
Enabled: false
|
data/Appraisals
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# See https://github.com/thoughtbot/appraisal for more information.
|
|
4
|
+
|
|
5
|
+
appraise "rails_7.1" do
|
|
6
|
+
gem "rails", "~> 7.1.0"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
appraise "rails_7.2" do
|
|
10
|
+
gem "rails", "~> 7.2.0"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
appraise "rails_8.0" do
|
|
14
|
+
gem "rails", "~> 8.0.0"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
appraise "rails_8.1" do
|
|
18
|
+
gem "rails", "~> 8.1.0"
|
|
19
|
+
end
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [3.0.0] - 2025-11-27
|
|
4
|
+
- Disabled fields name format validation
|
|
5
|
+
- Added scope functionality: _Active Field_ can now be limited to a specific context, allowing different sets of fields per group within the same _Customizable_ model.
|
|
6
|
+
- Added Appraisal to run tests across multiple Rails versions.
|
|
7
|
+
|
|
8
|
+
## [2.0.1] - 2025-04-09
|
|
9
|
+
- Fixed search with `nil` operator
|
|
10
|
+
|
|
11
|
+
## [2.0.0] - 2025-02-22
|
|
2
12
|
- Drop support for _Rails_ < 7.1
|
|
3
13
|
- Drop support for _Ruby_ < 3.1 (EOL)
|
|
4
14
|
- Added search functionality
|
data/README.md
CHANGED
|
@@ -38,6 +38,12 @@ classDiagram
|
|
|
38
38
|
All values are stored in a JSON (jsonb) field, which is a highly flexible column type capable of storing various data types,
|
|
39
39
|
such as booleans, strings, numbers, arrays, etc.
|
|
40
40
|
|
|
41
|
+
## Requirements
|
|
42
|
+
|
|
43
|
+
- Ruby 3.1+
|
|
44
|
+
- Rails 7.1+
|
|
45
|
+
- Postgres 15+ (17+ for search functionality)
|
|
46
|
+
|
|
41
47
|
## Installation
|
|
42
48
|
|
|
43
49
|
1. Install the gem and add it to your application's Gemfile by running:
|
|
@@ -849,6 +855,97 @@ IpFinder.new(active_field: ip_active_field).search(op: "eq", value: "127.0.0.1")
|
|
|
849
855
|
IpArrayFinder.new(active_field: ip_array_active_field).search(op: "#>=", value: 5)
|
|
850
856
|
```
|
|
851
857
|
|
|
858
|
+
### Scoping
|
|
859
|
+
|
|
860
|
+
The scoping feature enables multi-tenancy or context-based field definitions per model.
|
|
861
|
+
It allows you to define different sets of _Active Fields_ for different scopes (e.g., different tenants, organizations, or contexts).
|
|
862
|
+
|
|
863
|
+
**How it works:**
|
|
864
|
+
- Pass a `scope_method` parameter to `has_active_fields` to enable scoping for a _Customizable_ model. The method should return a value that identifies the scope (e.g., `tenant_id`, `organization_id`).
|
|
865
|
+
- The scope method's return value is automatically converted to a string and exposed as `active_fields_scope` on each _Customizable_ record. This value is used to match against _Active Field_ `scope` values.
|
|
866
|
+
- When an _Active Field_ has `scope` = `nil` (_global field_), it is available to all _Customizable_ records, regardless of their scope value.
|
|
867
|
+
- When an _Active Field_ has a `scope` != `nil` (_scope field_), it is only available to _Customizable_ records where `active_fields_scope` matches the `scope`.
|
|
868
|
+
|
|
869
|
+
```ruby
|
|
870
|
+
class User < ApplicationRecord
|
|
871
|
+
has_active_fields scope_method: :tenant_id
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
# Global active field (available to all users)
|
|
875
|
+
ActiveFields::Field::Text.create!(
|
|
876
|
+
name: "note",
|
|
877
|
+
customizable_type: "User",
|
|
878
|
+
scope: nil,
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
# Scoped active field (only available to users with tenant_id = "tenant_1")
|
|
882
|
+
ActiveFields::Field::Integer.create!(
|
|
883
|
+
name: "age",
|
|
884
|
+
customizable_type: "User",
|
|
885
|
+
scope: "tenant_1",
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
# Scoped active field (only available to users with tenant_id = "tenant_2")
|
|
889
|
+
ActiveFields::Field::Date.create!(
|
|
890
|
+
name: "registered_on",
|
|
891
|
+
customizable_type: "User",
|
|
892
|
+
scope: "tenant_2",
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
# Usage
|
|
896
|
+
user_1 = User.create!(tenant_id: "tenant_1")
|
|
897
|
+
user_1.active_fields # Returns `note` and `age`
|
|
898
|
+
|
|
899
|
+
user_2 = User.create!(tenant_id: "tenant_2")
|
|
900
|
+
user_2.active_fields # Returns `note` and `registered_on`
|
|
901
|
+
|
|
902
|
+
user_3 = User.create!(tenant_id: nil)
|
|
903
|
+
user_3.active_fields # Returns only `note`
|
|
904
|
+
|
|
905
|
+
# Query with scope
|
|
906
|
+
User.active_fields # Returns only `note`
|
|
907
|
+
User.active_fields(scope: "tenant_1") # Returns `note` and `age`
|
|
908
|
+
User.where_active_fields(filters) # Search by global fields only (`note`)
|
|
909
|
+
User.where_active_fields(filters, scope: "tenant_1") # Search by `note` and `registered_on`
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
**Handling scope changes:**
|
|
913
|
+
|
|
914
|
+
If you change the scope value of a _Customizable_ record (e.g., changing `tenant_id`), you must manually destroy _Active Values_ that are no longer available for that record.
|
|
915
|
+
The gem does not automatically handle this because the `scope_method` implementation is up to you, and therefore its change tracking is your responsibility.
|
|
916
|
+
However, there is a helper method that you could use to clear the _Active Values_ list: `clear_unavailable_active_values`.
|
|
917
|
+
|
|
918
|
+
**Example 1:** `scope_method` is a single database column.
|
|
919
|
+
|
|
920
|
+
```ruby
|
|
921
|
+
class User < ApplicationRecord
|
|
922
|
+
has_active_fields scope_method: :tenant_id
|
|
923
|
+
|
|
924
|
+
after_update :clear_unavailable_active_values, if: :saved_change_to_tenant_id?
|
|
925
|
+
end
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
**Example 2:** `scope_method` is a computed value from multiple columns.
|
|
929
|
+
|
|
930
|
+
```ruby
|
|
931
|
+
class User < ApplicationRecord
|
|
932
|
+
has_active_fields scope_method: :tenant_and_department_scope
|
|
933
|
+
|
|
934
|
+
# The scope method should return a string
|
|
935
|
+
def tenant_and_department_scope
|
|
936
|
+
"#{tenant_id}-#{department_id}"
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
after_update :clear_unavailable_active_values, if: :tenant_and_department_scope_changed?
|
|
940
|
+
|
|
941
|
+
private
|
|
942
|
+
|
|
943
|
+
def tenant_and_department_scope_changed?
|
|
944
|
+
saved_change_to_tenant_id? || saved_change_to_department_id?
|
|
945
|
+
end
|
|
946
|
+
end
|
|
947
|
+
```
|
|
948
|
+
|
|
852
949
|
### Localization (I18n)
|
|
853
950
|
|
|
854
951
|
The built-in _validators_ primarily use _Rails_ default error types.
|
|
@@ -861,9 +958,7 @@ For an example, refer to the [locale file](https://github.com/lassoid/active_fie
|
|
|
861
958
|
|
|
862
959
|
## Current Restrictions
|
|
863
960
|
|
|
864
|
-
1.
|
|
865
|
-
|
|
866
|
-
The gem is tested exclusively with _PostgreSQL_. Support for other databases is not guaranteed.
|
|
961
|
+
1. This gem requires _PostgreSQL_ and is not designed to support other database systems.
|
|
867
962
|
|
|
868
963
|
2. Updating some _Active Fields_ options may be unsafe.
|
|
869
964
|
|
|
@@ -901,6 +996,7 @@ active_field.available_customizable_types # Available Customizable types for thi
|
|
|
901
996
|
|
|
902
997
|
# Scopes:
|
|
903
998
|
ActiveFields::Field::Boolean.for("Post") # Collection of Active Fields registered for the specified Customizable type
|
|
999
|
+
ActiveFields::Field::Integer.for("User", scope: "main_tenant") # Collection of Active Fields available for the specified Customizable type with given scope
|
|
904
1000
|
```
|
|
905
1001
|
|
|
906
1002
|
### Values API
|
|
@@ -929,10 +1025,13 @@ customizable = Post.take
|
|
|
929
1025
|
customizable.active_values # `has_many` association with Active Values linked to this Customizable
|
|
930
1026
|
|
|
931
1027
|
# Methods:
|
|
932
|
-
customizable.active_fields # Collection of Active Fields
|
|
933
|
-
|
|
1028
|
+
customizable.active_fields # Collection of Active Fields available for this record
|
|
1029
|
+
customizable.active_fields_scope # Scope value for this record
|
|
1030
|
+
Post.active_fields # Collection of Active Fields available for this model
|
|
1031
|
+
User.active_fields(scope: "main_tenant") # Collection of Active Fields available for this model and given scope
|
|
934
1032
|
Post.allowed_active_fields_type_names # Active Fields type names allowed for this Customizable model
|
|
935
1033
|
Post.allowed_active_fields_class_names # Active Fields class names allowed for this Customizable model
|
|
1034
|
+
User.active_fields_scope_method # Scope method for this model
|
|
936
1035
|
|
|
937
1036
|
# Create, update or destroy Active Values.
|
|
938
1037
|
customizable.active_fields_attributes = [
|
|
@@ -963,6 +1062,10 @@ customizable.active_values_attributes = attributes
|
|
|
963
1062
|
# `form.fields_for :active_fields, customizable.initialize_active_values`.
|
|
964
1063
|
customizable.initialize_active_values
|
|
965
1064
|
|
|
1065
|
+
# Destroys Active Values that are no longer associated with Active Fields available for this record.
|
|
1066
|
+
# Call this method after changing the scope value to ensure all Active Values are valid.
|
|
1067
|
+
customizable.clear_unavailable_active_values
|
|
1068
|
+
|
|
966
1069
|
# Query Customizables by Active Values.
|
|
967
1070
|
Post.where_active_fields(
|
|
968
1071
|
[
|
|
@@ -971,6 +1074,11 @@ Post.where_active_fields(
|
|
|
971
1074
|
{ n: "boolean", op: "!=", v: false }, # compact form (string or symbol keys)
|
|
972
1075
|
],
|
|
973
1076
|
)
|
|
1077
|
+
# Search with given scope.
|
|
1078
|
+
User.where_active_fields(
|
|
1079
|
+
filters,
|
|
1080
|
+
scope: "main_tenant",
|
|
1081
|
+
)
|
|
974
1082
|
```
|
|
975
1083
|
|
|
976
1084
|
### Global Config
|
|
@@ -1013,14 +1121,7 @@ and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
|
1013
1121
|
## Contributing
|
|
1014
1122
|
|
|
1015
1123
|
Bug reports and pull requests are welcome on GitHub at https://github.com/lassoid/active_fields.
|
|
1016
|
-
This project is intended to be a safe, welcoming space for collaboration, and contributors
|
|
1017
|
-
are expected to adhere to the [code of conduct](https://github.com/lassoid/active_fields/blob/main/CODE_OF_CONDUCT.md).
|
|
1018
1124
|
|
|
1019
1125
|
## License
|
|
1020
1126
|
|
|
1021
1127
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
1022
|
-
|
|
1023
|
-
## Code of Conduct
|
|
1024
|
-
|
|
1025
|
-
Everyone interacting in the ActiveFields project's codebases, issue trackers, chat rooms and mailing lists
|
|
1026
|
-
is expected to follow the [code of conduct](https://github.com/lassoid/active_fields/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
|
@@ -6,14 +6,12 @@ module ActiveFields
|
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
|
|
8
8
|
included do
|
|
9
|
-
# rubocop:disable Rails/ReflectionClassName
|
|
10
9
|
has_many :active_values,
|
|
11
10
|
class_name: ActiveFields.config.value_class_name,
|
|
12
11
|
as: :customizable,
|
|
13
12
|
inverse_of: :customizable,
|
|
14
13
|
autosave: true,
|
|
15
14
|
dependent: :destroy
|
|
16
|
-
# rubocop:enable Rails/ReflectionClassName
|
|
17
15
|
|
|
18
16
|
# Searches customizables by active_values.
|
|
19
17
|
#
|
|
@@ -25,6 +23,10 @@ module ActiveFields
|
|
|
25
23
|
# - <tt>:op</tt> or <tt>:operator</tt> key specifying search operation or operator;
|
|
26
24
|
# - <tt>:v</tt> or <tt>:value</tt> key specifying search value.
|
|
27
25
|
#
|
|
26
|
+
# Optionally, a <tt>:scope</tt> keyword argument can be provided to expand the search
|
|
27
|
+
# to include both global fields (where scope is nil) and scoped fields for the given scope.
|
|
28
|
+
# When <tt>:scope</tt> is omitted or nil, only global fields are searched.
|
|
29
|
+
#
|
|
28
30
|
# Example:
|
|
29
31
|
#
|
|
30
32
|
# # Array of hashes
|
|
@@ -47,7 +49,13 @@ module ActiveFields
|
|
|
47
49
|
#
|
|
48
50
|
# # Params (must be permitted)
|
|
49
51
|
# CustomizableModel.where_active_fields(permitted_params)
|
|
50
|
-
|
|
52
|
+
#
|
|
53
|
+
# # Scoped
|
|
54
|
+
# CustomizableModel.where_active_fields(
|
|
55
|
+
# filters,
|
|
56
|
+
# scope: Current.tenant.id,
|
|
57
|
+
# )
|
|
58
|
+
scope :where_active_fields, ->(filters, scope: nil) do
|
|
51
59
|
filters = filters.to_h if filters.respond_to?(:permitted?)
|
|
52
60
|
|
|
53
61
|
unless filters.is_a?(Array) || filters.is_a?(Hash)
|
|
@@ -57,23 +65,23 @@ module ActiveFields
|
|
|
57
65
|
# Handle `fields_for` params
|
|
58
66
|
filters = filters.values if filters.is_a?(Hash)
|
|
59
67
|
|
|
60
|
-
active_fields_by_name = active_fields.index_by(&:name)
|
|
68
|
+
active_fields_by_name = active_fields(scope: scope).index_by(&:name)
|
|
61
69
|
|
|
62
|
-
filters.inject(self) do |
|
|
70
|
+
filters.inject(self) do |query, filter|
|
|
63
71
|
filter = filter.to_h if filter.respond_to?(:permitted?)
|
|
64
72
|
filter = filter.with_indifferent_access
|
|
65
73
|
|
|
66
74
|
active_field = active_fields_by_name[filter[:n] || filter[:name]]
|
|
67
|
-
next
|
|
68
|
-
next
|
|
75
|
+
next query if active_field.nil?
|
|
76
|
+
next query if active_field.value_finder.nil?
|
|
69
77
|
|
|
70
78
|
active_values = active_field.value_finder.search(
|
|
71
79
|
op: filter[:op] || filter[:operator],
|
|
72
80
|
value: filter[:v] || filter[:value],
|
|
73
81
|
)
|
|
74
|
-
next
|
|
82
|
+
next query if active_values.nil?
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
query.where(id: active_values.select(:customizable_id))
|
|
77
85
|
end
|
|
78
86
|
end
|
|
79
87
|
|
|
@@ -81,9 +89,13 @@ module ActiveFields
|
|
|
81
89
|
end
|
|
82
90
|
|
|
83
91
|
class_methods do
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
# Returns the collection of active fields associated with this customizable model.
|
|
93
|
+
# You can provide an optional <tt>:scope</tt> parameter
|
|
94
|
+
# to retrieve active fields available for a specific scope, not just global fields.
|
|
95
|
+
def active_fields(scope: nil)
|
|
96
|
+
scope = nil if active_fields_scope_method.nil?
|
|
97
|
+
|
|
98
|
+
ActiveFields.config.field_base_class.for(name, scope: scope)
|
|
87
99
|
end
|
|
88
100
|
|
|
89
101
|
# Returns active fields type names allowed for this customizable model.
|
|
@@ -97,7 +109,17 @@ module ActiveFields
|
|
|
97
109
|
end
|
|
98
110
|
end
|
|
99
111
|
|
|
100
|
-
|
|
112
|
+
# Collection of active fields registered for this customizable.
|
|
113
|
+
def active_fields
|
|
114
|
+
self.class.active_fields(scope: active_fields_scope)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Scope value for the active fields collection.
|
|
118
|
+
def active_fields_scope
|
|
119
|
+
return if self.class.active_fields_scope_method.nil?
|
|
120
|
+
|
|
121
|
+
send(self.class.active_fields_scope_method)&.to_s
|
|
122
|
+
end
|
|
101
123
|
|
|
102
124
|
# Assigns the given attributes to the active_values association.
|
|
103
125
|
#
|
|
@@ -179,5 +201,29 @@ module ActiveFields
|
|
|
179
201
|
|
|
180
202
|
active_values
|
|
181
203
|
end
|
|
204
|
+
|
|
205
|
+
# Destroys all active_values that are no longer available for this customizable
|
|
206
|
+
# (e.g. after its scope has changed).
|
|
207
|
+
#
|
|
208
|
+
# This method is suitable for customizables with scope functionality enabled
|
|
209
|
+
# (when `has_active_fields` is called with a `scope_method:` parameter).
|
|
210
|
+
# When a customizable's scope value changes,
|
|
211
|
+
# some active_values may reference active_fields that are no longer available for the new scope.
|
|
212
|
+
# This method identifies and destroys those orphaned active_values.
|
|
213
|
+
#
|
|
214
|
+
# Example:
|
|
215
|
+
#
|
|
216
|
+
# class User < ApplicationRecord
|
|
217
|
+
# has_active_fields scope_method: :tenant_id
|
|
218
|
+
#
|
|
219
|
+
# after_update :clear_unavailable_active_values, if: :saved_change_to_tenant_id?
|
|
220
|
+
# end
|
|
221
|
+
def clear_unavailable_active_values
|
|
222
|
+
available_field_ids = active_fields.pluck(:id)
|
|
223
|
+
|
|
224
|
+
active_values.select do |active_value|
|
|
225
|
+
available_field_ids.exclude?(active_value.active_field_id)
|
|
226
|
+
end.each(&:destroy)
|
|
227
|
+
end
|
|
182
228
|
end
|
|
183
229
|
end
|
|
@@ -6,23 +6,28 @@ module ActiveFields
|
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
|
|
8
8
|
included do
|
|
9
|
-
# rubocop:disable Rails/ReflectionClassName
|
|
10
9
|
has_many :active_values,
|
|
11
10
|
class_name: ActiveFields.config.value_class_name,
|
|
12
11
|
foreign_key: :active_field_id,
|
|
13
12
|
inverse_of: :active_field,
|
|
14
13
|
dependent: :destroy
|
|
15
|
-
# rubocop:enable Rails/ReflectionClassName
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
# Returns active fields for the given customizable type, including both
|
|
16
|
+
# scoped fields (when scope parameter is provided) and global fields (where scope is nil).
|
|
17
|
+
scope :for, ->(customizable_type, scope: nil) do
|
|
18
|
+
# Global fields are available to all records of the given customizable type.
|
|
19
|
+
scopes = [scope, nil].uniq
|
|
20
|
+
where(customizable_type: customizable_type, scope: scopes)
|
|
21
|
+
end
|
|
18
22
|
|
|
19
23
|
validates :type, presence: true
|
|
20
|
-
validates :name, presence: true
|
|
21
|
-
|
|
24
|
+
validates :name, presence: true
|
|
25
|
+
validate :validate_name_uniqueness
|
|
22
26
|
validate :validate_default_value
|
|
23
27
|
validate :validate_customizable_model_allows_type
|
|
24
28
|
|
|
25
29
|
after_initialize :set_defaults
|
|
30
|
+
before_validation :set_scope
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
class_methods do
|
|
@@ -91,7 +96,7 @@ module ActiveFields
|
|
|
91
96
|
|
|
92
97
|
# Returns customizable types that allow this field type.
|
|
93
98
|
#
|
|
94
|
-
#
|
|
99
|
+
# NOTE:
|
|
95
100
|
# - The customizable model must be loaded to appear in this list.
|
|
96
101
|
# Relationships between customizable models and field types are established in the `has_active_fields` method,
|
|
97
102
|
# which is typically called within the customizable model.
|
|
@@ -106,6 +111,26 @@ module ActiveFields
|
|
|
106
111
|
|
|
107
112
|
private
|
|
108
113
|
|
|
114
|
+
# NOTE: The uniqueness constraint in the DB does not fully enforce this validation:
|
|
115
|
+
# Records where scope is NULL do not prevent the existence of records with the same
|
|
116
|
+
# [name, customizable_type] but with a non-NULL scope, and vice versa.
|
|
117
|
+
def validate_name_uniqueness
|
|
118
|
+
base_scope =
|
|
119
|
+
ActiveFields.config.field_base_class.excluding(self).where(customizable_type: customizable_type, name: name)
|
|
120
|
+
|
|
121
|
+
if scope.nil?
|
|
122
|
+
if base_scope.exists?
|
|
123
|
+
errors.add(:name, :taken)
|
|
124
|
+
return false
|
|
125
|
+
end
|
|
126
|
+
elsif base_scope.exists?(scope: [scope, nil])
|
|
127
|
+
errors.add(:name, :taken)
|
|
128
|
+
return false
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
true
|
|
132
|
+
end
|
|
133
|
+
|
|
109
134
|
def validate_default_value
|
|
110
135
|
validator = value_validator
|
|
111
136
|
return if validator.validate(default_value)
|
|
@@ -130,5 +155,12 @@ module ActiveFields
|
|
|
130
155
|
end
|
|
131
156
|
|
|
132
157
|
def set_defaults; end
|
|
158
|
+
|
|
159
|
+
# Forces scope to nil if the customizable model does not have a scope method.
|
|
160
|
+
def set_scope
|
|
161
|
+
return if customizable_model.nil?
|
|
162
|
+
|
|
163
|
+
self.scope = nil if customizable_model.active_fields_scope_method.nil?
|
|
164
|
+
end
|
|
133
165
|
end
|
|
134
166
|
end
|
|
@@ -7,12 +7,10 @@ module ActiveFields
|
|
|
7
7
|
|
|
8
8
|
included do
|
|
9
9
|
belongs_to :customizable, polymorphic: true, optional: false, inverse_of: :active_values
|
|
10
|
-
# rubocop:disable Rails/ReflectionClassName
|
|
11
10
|
belongs_to :active_field,
|
|
12
11
|
class_name: ActiveFields.config.field_base_class_name,
|
|
13
12
|
optional: false,
|
|
14
13
|
inverse_of: :active_values
|
|
15
|
-
# rubocop:enable Rails/ReflectionClassName
|
|
16
14
|
|
|
17
15
|
validates :active_field, uniqueness: { scope: :customizable }
|
|
18
16
|
validate :validate_value
|
|
@@ -61,7 +59,14 @@ module ActiveFields
|
|
|
61
59
|
|
|
62
60
|
def validate_customizable_allowed
|
|
63
61
|
return true if active_field.nil?
|
|
64
|
-
|
|
62
|
+
|
|
63
|
+
if customizable_type != active_field.customizable_type
|
|
64
|
+
errors.add(:customizable, :invalid)
|
|
65
|
+
return false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
return true if active_field.scope.nil?
|
|
69
|
+
return true if customizable&.active_fields_scope == active_field.scope
|
|
65
70
|
|
|
66
71
|
errors.add(:customizable, :invalid)
|
|
67
72
|
false
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddScopeToActiveFields < ActiveRecord::Migration[7.1]
|
|
4
|
+
def change
|
|
5
|
+
change_table :active_fields do |t|
|
|
6
|
+
t.string :scope
|
|
7
|
+
|
|
8
|
+
t.remove_index %i[name customizable_type], unique: true
|
|
9
|
+
t.remove_index :customizable_type
|
|
10
|
+
|
|
11
|
+
t.index %i[customizable_type scope name], unique: true, nulls_not_distinct: true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "puma"
|
|
6
|
+
gem "propshaft"
|
|
7
|
+
gem "importmap-rails"
|
|
8
|
+
gem "stimulus-rails"
|
|
9
|
+
gem "rails", "~> 7.1.0"
|
|
10
|
+
|
|
11
|
+
group :development do
|
|
12
|
+
gem "web-console"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "puma"
|
|
6
|
+
gem "propshaft"
|
|
7
|
+
gem "importmap-rails"
|
|
8
|
+
gem "stimulus-rails"
|
|
9
|
+
gem "rails", "~> 7.2.0"
|
|
10
|
+
|
|
11
|
+
group :development do
|
|
12
|
+
gem "web-console"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "puma"
|
|
6
|
+
gem "propshaft"
|
|
7
|
+
gem "importmap-rails"
|
|
8
|
+
gem "stimulus-rails"
|
|
9
|
+
gem "rails", "~> 8.0.0"
|
|
10
|
+
|
|
11
|
+
group :development do
|
|
12
|
+
gem "web-console"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "puma"
|
|
6
|
+
gem "propshaft"
|
|
7
|
+
gem "importmap-rails"
|
|
8
|
+
gem "stimulus-rails"
|
|
9
|
+
gem "rails", "~> 8.1.0"
|
|
10
|
+
|
|
11
|
+
group :development do
|
|
12
|
+
gem "web-console"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gemspec path: "../"
|
|
@@ -6,13 +6,13 @@ module ActiveFields
|
|
|
6
6
|
def serialize(value)
|
|
7
7
|
return unless value.is_a?(Array)
|
|
8
8
|
|
|
9
|
-
value.map { super(
|
|
9
|
+
value.map { |element| super(element) }
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def deserialize(value)
|
|
13
13
|
return unless value.is_a?(Array)
|
|
14
14
|
|
|
15
|
-
value.map { super(
|
|
15
|
+
value.map { |element| super(element) }
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -6,13 +6,13 @@ module ActiveFields
|
|
|
6
6
|
def serialize(value)
|
|
7
7
|
return unless value.is_a?(Array)
|
|
8
8
|
|
|
9
|
-
value.map { super(
|
|
9
|
+
value.map { |element| super(element) }
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def deserialize(value)
|
|
13
13
|
return unless value.is_a?(Array)
|
|
14
14
|
|
|
15
|
-
value.map { super(
|
|
15
|
+
value.map { |element| super(element) }
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -6,13 +6,13 @@ module ActiveFields
|
|
|
6
6
|
def serialize(value)
|
|
7
7
|
return unless value.is_a?(Array)
|
|
8
8
|
|
|
9
|
-
value.map { super(
|
|
9
|
+
value.map { |element| super(element) }
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def deserialize(value)
|
|
13
13
|
return unless value.is_a?(Array)
|
|
14
14
|
|
|
15
|
-
value.map { super(
|
|
15
|
+
value.map { |element| super(element) }
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -6,13 +6,13 @@ module ActiveFields
|
|
|
6
6
|
def serialize(value)
|
|
7
7
|
return unless value.is_a?(Array)
|
|
8
8
|
|
|
9
|
-
value.map { super(
|
|
9
|
+
value.map { |element| super(element) }
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def deserialize(value)
|
|
13
13
|
return unless value.is_a?(Array)
|
|
14
14
|
|
|
15
|
-
value.map { super(
|
|
15
|
+
value.map { |element| super(element) }
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|