active_fields 2.0.1 → 3.0.1

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -2
  3. data/Appraisals +19 -0
  4. data/CHANGELOG.md +9 -1
  5. data/README.md +202 -26
  6. data/Rakefile +0 -2
  7. data/app/models/concerns/active_fields/customizable_concern.rb +66 -14
  8. data/app/models/concerns/active_fields/field_concern.rb +38 -4
  9. data/app/models/concerns/active_fields/value_concern.rb +8 -1
  10. data/db/migrate/20250229230000_add_scope_to_active_fields.rb +14 -0
  11. data/gemfiles/rails_7.1.gemfile +15 -0
  12. data/gemfiles/rails_7.2.gemfile +15 -0
  13. data/gemfiles/rails_8.0.gemfile +15 -0
  14. data/gemfiles/rails_8.1.gemfile +15 -0
  15. data/lib/active_fields/casters/date_array_caster.rb +2 -2
  16. data/lib/active_fields/casters/date_time_array_caster.rb +2 -2
  17. data/lib/active_fields/casters/decimal_array_caster.rb +2 -2
  18. data/lib/active_fields/casters/enum_array_caster.rb +2 -2
  19. data/lib/active_fields/casters/integer_array_caster.rb +2 -2
  20. data/lib/active_fields/casters/text_array_caster.rb +2 -2
  21. data/lib/active_fields/config.rb +2 -2
  22. data/lib/active_fields/finders/array_finder.rb +8 -2
  23. data/lib/active_fields/has_active_fields.rb +3 -1
  24. data/lib/active_fields/version.rb +1 -1
  25. data/lib/generators/active_fields/scaffold/templates/controllers/active_fields_controller.rb +18 -18
  26. data/lib/generators/active_fields/scaffold/templates/javascript/controllers/active_field_form_controller.js +19 -0
  27. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_boolean.html.erb +22 -1
  28. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date.html.erb +22 -1
  29. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date_array.html.erb +22 -1
  30. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime.html.erb +22 -1
  31. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime_array.html.erb +22 -1
  32. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal.html.erb +22 -1
  33. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal_array.html.erb +22 -1
  34. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum.html.erb +22 -1
  35. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum_array.html.erb +22 -1
  36. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer.html.erb +22 -1
  37. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer_array.html.erb +22 -1
  38. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text.html.erb +22 -1
  39. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text_array.html.erb +22 -1
  40. data/lib/generators/active_fields/scaffold/templates/views/active_fields/index.html.erb +2 -0
  41. metadata +24 -4
  42. data/CODE_OF_CONDUCT.md +0 -84
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c94bb84092249c9db341f69ec79233da8440b8ef74c00475c5c8acb6e42a290
4
- data.tar.gz: 7653a7859137a53d465ccc2398f055f064105949f0a3be7e1e7fc5f3636ee240
3
+ metadata.gz: 86d3c04e24cae1b29e2f3a4957cec89fe64132c6b3d7f5432f16c6a84a0d8914
4
+ data.tar.gz: d1b6d2385b47913edf57ca4709e73eaf04f64f7d6d87a3c5e028b092f3b1a97e
5
5
  SHA512:
6
- metadata.gz: 57eecfc4cb3cf040dfcc2daf59a804b33ea5b3f9744cde6931c8fa0f9777eeeb067d3895b33ebbf927c5b74c66290f00546986d472d038c11df700d8092631d7
7
- data.tar.gz: 1e923ede58bd193079334b3fcb9e193fdd61b56d6d73ffdf977a626618d2e308787641c8194ebc806113141b9dd66a3e5fbea106c0d0ded699c866a10a6b8d0b
6
+ metadata.gz: abca69b126eacb5fc015d29fc0b7b67bc39e0905c68b8fb1a1fe86a80065838c80a5650fea5beac4bafdee1d123287aad405566115a58e2f0b39fa2c23fe33d5
7
+ data.tar.gz: f3e0e310b0a1a33f11c3226e2e678fe84bd6787609c189afe6e086b7e039b0852b13df0fcdf08759e06dc1ccf931ba8b9eeff00137faae843a26b02267f77954
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.3
12
- TargetRailsVersion: 8.0
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,6 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
- # [2.0.1] - 2025-04-09
3
+ ## [3.0.1] - 2025-11-28
4
+ - Fixed an issue where searching with `v: false` was treated as `nil` due to `false || nil` logic
5
+
6
+ ## [3.0.0] - 2025-11-27
7
+ - Disabled fields name format validation
8
+ - 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.
9
+ - Added Appraisal to run tests across multiple Rails versions.
10
+
11
+ ## [2.0.1] - 2025-04-09
4
12
  - Fixed search with `nil` operator
5
13
 
6
14
  ## [2.0.0] - 2025-02-22
data/README.md CHANGED
@@ -5,7 +5,38 @@
5
5
  [![Github Actions CI](https://github.com/lassoid/active_fields/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/lassoid/active_fields/actions/workflows/main.yml)
6
6
 
7
7
  **ActiveFields** is a _Rails_ plugin that implements the _Entity-Attribute-Value_ (_EAV_) pattern,
8
- enabling the addition of custom fields to any model at runtime without requiring changes to the database schema.
8
+ enabling the addition of custom fields to any model at runtime without requiring changes to the database schema or application code.
9
+
10
+ It may look similar to other gems like [attr_json](https://github.com/jrochkind/attr_json) or [store_attribute](https://github.com/palkan/store_attribute), but it solves a fundamentally different problem. While those tools allow you to add fields without migrations, they still require developer work - you must write code to define each field.
11
+
12
+ The main use case of _EAV_ in general, and _ActiveFields_ in particular, is to enable **any application user** (not just developers) to add their own fields. Not just without migrations, but without touching the source code at all. These are truly data-driven fields that can be created, modified, and managed entirely through your application's interface.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Key Concepts](#key-concepts)
17
+ - [Models Structure](#models-structure)
18
+ - [Requirements](#requirements)
19
+ - [Installation](#installation)
20
+ - [Field Types](#field-types)
21
+ - [Fields Base Attributes](#fields-base-attributes)
22
+ - [Field Types Summary](#field-types-summary)
23
+ - [Search Functionality](#search-functionality)
24
+ - [Configuration](#configuration)
25
+ - [Limiting Field Types for a Customizable](#limiting-field-types-for-a-customizable)
26
+ - [Customizing Internal Model Classes](#customizing-internal-model-classes)
27
+ - [Adding Custom Field Types](#adding-custom-field-types)
28
+ - [Multi-tenancy (scoping)](#multi-tenancy-scoping)
29
+ - [Localization (I18n)](#localization-i18n)
30
+ - [Current Restrictions](#current-restrictions)
31
+ - [API Overview](#api-overview)
32
+ - [Fields API](#fields-api)
33
+ - [Values API](#values-api)
34
+ - [Customizable API](#customizable-api)
35
+ - [Global Config](#global-config)
36
+ - [Registry](#registry)
37
+ - [Development](#development)
38
+ - [Contributing](#contributing)
39
+ - [License](#license)
9
40
 
10
41
  ## Key Concepts
11
42
 
@@ -38,6 +69,12 @@ classDiagram
38
69
  All values are stored in a JSON (jsonb) field, which is a highly flexible column type capable of storing various data types,
39
70
  such as booleans, strings, numbers, arrays, etc.
40
71
 
72
+ ## Requirements
73
+
74
+ - Ruby 3.1+
75
+ - Rails 7.1+
76
+ - Postgres 15+ (17+ for search functionality)
77
+
41
78
  ## Installation
42
79
 
43
80
  1. Install the gem and add it to your application's Gemfile by running:
@@ -286,6 +323,9 @@ classDiagram
286
323
  All _Active Field_ model names start with `ActiveFields::Field`.
287
324
  We replace it with `**` for conciseness.
288
325
 
326
+ <details>
327
+ <summary><strong>Table</strong></summary>
328
+
289
329
  | Active Field model | Type name | Attributes | Options |
290
330
  |---------------------------------|------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
291
331
  | `**::Boolean` | `boolean` | `default_value`<br>(`boolean` or `nil`) | `required`(`boolean`) - the value must not be `false`<br>`nullable`(`boolean`) - the value could be `nil` |
@@ -303,6 +343,8 @@ We replace it with `**` for conciseness.
303
343
  | `**::TextArray` | `text_array` | `default_value`<br>(`array[string]`) | `min_length`(`integer`) - minimum value length allowed, for each element<br>`max_length`(`integer`) - maximum value length allowed, for each element<br>`min_size`(`integer`) - minimum value size<br>`max_size`(`integer`) - maximum value size |
304
344
  | _Your custom class can be here_ | _..._ | _..._ | _..._ |
305
345
 
346
+ </details>
347
+
306
348
  **Note:** Options marked with **\*** are mandatory.
307
349
 
308
350
  ### Search Functionality
@@ -348,14 +390,18 @@ Key details:
348
390
 
349
391
  Supported _operations_ and _operators_ for each _Active Field_ type are listed below.
350
392
 
351
- #### Boolean
393
+ <details>
394
+ <summary><strong>Boolean</strong></summary>
352
395
 
353
396
  | Operation | Operator | Description |
354
397
  |------------|----------|-----------------------------|
355
398
  | `eq` | `=` | Value is equal to given |
356
399
  | `not_eq` | `!=` | Value is not equal to given |
357
400
 
358
- #### Date
401
+ </details>
402
+
403
+ <details>
404
+ <summary><strong>Date</strong></summary>
359
405
 
360
406
  | Operation | Operator | Description |
361
407
  |------------|----------|-----------------------------------------|
@@ -366,7 +412,10 @@ Supported _operations_ and _operators_ for each _Active Field_ type are listed b
366
412
  | `lt` | `<` | Value is less than given |
367
413
  | `lteq` | `<=` | Value is less than or equal to given |
368
414
 
369
- #### DateArray
415
+ </details>
416
+
417
+ <details>
418
+ <summary><strong>DateArray</strong></summary>
370
419
 
371
420
  | Operation | Operator | Description |
372
421
  |---------------|----------|----------------------------------------------------------------|
@@ -387,7 +436,10 @@ Supported _operations_ and _operators_ for each _Active Field_ type are listed b
387
436
  | `size_lt` | `#<` | Array value size is less than given |
388
437
  | `size_lteq` | `#<=` | Array value size is less than or equal to given |
389
438
 
390
- #### DateTime
439
+ </details>
440
+
441
+ <details>
442
+ <summary><strong>DateTime</strong></summary>
391
443
 
392
444
  | Operation | Operator | Description |
393
445
  |------------|----------|-----------------------------------------|
@@ -398,7 +450,10 @@ Supported _operations_ and _operators_ for each _Active Field_ type are listed b
398
450
  | `lt` | `<` | Value is less than given |
399
451
  | `lteq` | `<=` | Value is greater than or equal to given |
400
452
 
401
- #### DateTimeArray
453
+ </details>
454
+
455
+ <details>
456
+ <summary><strong>DateTimeArray</strong></summary>
402
457
 
403
458
  | Operation | Operator | Description |
404
459
  |---------------|----------|----------------------------------------------------------------|
@@ -419,7 +474,10 @@ Supported _operations_ and _operators_ for each _Active Field_ type are listed b
419
474
  | `size_lt` | `#<` | Array value size is less than given |
420
475
  | `size_lteq` | `#<=` | Array value size is less than or equal to given |
421
476
 
422
- #### Decimal
477
+ </details>
478
+
479
+ <details>
480
+ <summary><strong>Decimal</strong></summary>
423
481
 
424
482
  | Operation | Operator | Description |
425
483
  |------------|----------|-----------------------------------------|
@@ -430,7 +488,10 @@ Supported _operations_ and _operators_ for each _Active Field_ type are listed b
430
488
  | `lt` | `<` | Value is less than given |
431
489
  | `lteq` | `<=` | Value is greater than or equal to given |
432
490
 
433
- #### DecimalArray
491
+ </details>
492
+
493
+ <details>
494
+ <summary><strong>DecimalArray</strong></summary>
434
495
 
435
496
  | Operation | Operator | Description |
436
497
  |---------------|----------|----------------------------------------------------------------|
@@ -451,14 +512,20 @@ Supported _operations_ and _operators_ for each _Active Field_ type are listed b
451
512
  | `size_lt` | `#<` | Array value size is less than given |
452
513
  | `size_lteq` | `#<=` | Array value size is less than or equal to given |
453
514
 
454
- #### Enum
515
+ </details>
516
+
517
+ <details>
518
+ <summary><strong>Enum</strong></summary>
455
519
 
456
520
  | Operation | Operator | Description |
457
521
  |------------|----------|-----------------------------|
458
522
  | `eq` | `=` | Value is equal to given |
459
523
  | `not_eq` | `!=` | Value is not equal to given |
460
524
 
461
- #### EnumArray
525
+ </details>
526
+
527
+ <details>
528
+ <summary><strong>EnumArray</strong></summary>
462
529
 
463
530
  | Operation | Operator | Description |
464
531
  |---------------|----------|----------------------------------------------------|
@@ -471,7 +538,10 @@ Supported _operations_ and _operators_ for each _Active Field_ type are listed b
471
538
  | `size_lt` | `#<` | Array value size is less than given |
472
539
  | `size_lteq` | `#<=` | Array value size is less than or equal to given |
473
540
 
474
- #### Integer
541
+ </details>
542
+
543
+ <details>
544
+ <summary><strong>Integer</strong></summary>
475
545
 
476
546
  | Operation | Operator | Description |
477
547
  |------------|----------|-----------------------------------------|
@@ -482,7 +552,10 @@ Supported _operations_ and _operators_ for each _Active Field_ type are listed b
482
552
  | `lt` | `<` | Value is less than given |
483
553
  | `lteq` | `<=` | Value is greater than or equal to given |
484
554
 
485
- #### IntegerArray
555
+ </details>
556
+
557
+ <details>
558
+ <summary><strong>IntegerArray</strong></summary>
486
559
 
487
560
  | Operation | Operator | Description |
488
561
  |---------------|----------|----------------------------------------------------------------|
@@ -503,7 +576,10 @@ Supported _operations_ and _operators_ for each _Active Field_ type are listed b
503
576
  | `size_lt` | `#<` | Array value size is less than given |
504
577
  | `size_lteq` | `#<=` | Array value size is less than or equal to given |
505
578
 
506
- #### Text
579
+ </details>
580
+
581
+ <details>
582
+ <summary><strong>Text</strong></summary>
507
583
 
508
584
  | Operation | Operator | Description |
509
585
  |-------------------|----------|-------------------------------------------------------------|
@@ -522,7 +598,10 @@ Supported _operations_ and _operators_ for each _Active Field_ type are listed b
522
598
  | `not_iend_with` | `!$*` | Value doesn't end with given substring (case-insensitive) |
523
599
  | `not_icontain` | `!~*` | Value doesn't contain given substring (case-insensitive) |
524
600
 
525
- #### TextArray
601
+ </details>
602
+
603
+ <details>
604
+ <summary><strong>TextArray</strong></summary>
526
605
 
527
606
  | Operation | Operator | Description |
528
607
  |------------------|----------|-------------------------------------------------------------|
@@ -537,6 +616,8 @@ Supported _operations_ and _operators_ for each _Active Field_ type are listed b
537
616
  | `size_lt` | `#<` | Array value size is less than given |
538
617
  | `size_lteq` | `#<=` | Array value size is less than or equal to given |
539
618
 
619
+ </details>
620
+
540
621
  ## Configuration
541
622
 
542
623
  ### Limiting Field Types for a Customizable
@@ -849,6 +930,97 @@ IpFinder.new(active_field: ip_active_field).search(op: "eq", value: "127.0.0.1")
849
930
  IpArrayFinder.new(active_field: ip_array_active_field).search(op: "#>=", value: 5)
850
931
  ```
851
932
 
933
+ ### Multi-tenancy (scoping)
934
+
935
+ The scoping feature enables multi-tenancy or context-based field definitions per model.
936
+ It allows you to define different sets of _Active Fields_ for different scopes (e.g., different tenants, organizations, or contexts).
937
+
938
+ **How it works:**
939
+ - 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`).
940
+ - 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.
941
+ - When an _Active Field_ has `scope` = `nil` (_global field_), it is available to all _Customizable_ records, regardless of their scope value.
942
+ - When an _Active Field_ has a `scope` != `nil` (_scope field_), it is only available to _Customizable_ records where `active_fields_scope` matches the `scope`.
943
+
944
+ ```ruby
945
+ class User < ApplicationRecord
946
+ has_active_fields scope_method: :tenant_id
947
+ end
948
+
949
+ # Global active field (available to all users)
950
+ ActiveFields::Field::Text.create!(
951
+ name: "note",
952
+ customizable_type: "User",
953
+ scope: nil,
954
+ )
955
+
956
+ # Scoped active field (only available to users with tenant_id = "tenant_1")
957
+ ActiveFields::Field::Integer.create!(
958
+ name: "age",
959
+ customizable_type: "User",
960
+ scope: "tenant_1",
961
+ )
962
+
963
+ # Scoped active field (only available to users with tenant_id = "tenant_2")
964
+ ActiveFields::Field::Date.create!(
965
+ name: "registered_on",
966
+ customizable_type: "User",
967
+ scope: "tenant_2",
968
+ )
969
+
970
+ # Usage
971
+ user_1 = User.create!(tenant_id: "tenant_1")
972
+ user_1.active_fields # Returns `note` and `age`
973
+
974
+ user_2 = User.create!(tenant_id: "tenant_2")
975
+ user_2.active_fields # Returns `note` and `registered_on`
976
+
977
+ user_3 = User.create!(tenant_id: nil)
978
+ user_3.active_fields # Returns only `note`
979
+
980
+ # Query with scope
981
+ User.active_fields # Returns only `note`
982
+ User.active_fields(scope: "tenant_1") # Returns `note` and `age`
983
+ User.where_active_fields(filters) # Search by global fields only (`note`)
984
+ User.where_active_fields(filters, scope: "tenant_1") # Search by `note` and `registered_on`
985
+ ```
986
+
987
+ **Handling scope changes:**
988
+
989
+ 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.
990
+ The gem does not automatically handle this because the `scope_method` implementation is up to you, and therefore its change tracking is your responsibility.
991
+ However, there is a helper method that you could use to clear the _Active Values_ list: `clear_unavailable_active_values`.
992
+
993
+ **Example 1:** `scope_method` is a single database column.
994
+
995
+ ```ruby
996
+ class User < ApplicationRecord
997
+ has_active_fields scope_method: :tenant_id
998
+
999
+ after_update :clear_unavailable_active_values, if: :saved_change_to_tenant_id?
1000
+ end
1001
+ ```
1002
+
1003
+ **Example 2:** `scope_method` is a computed value from multiple columns.
1004
+
1005
+ ```ruby
1006
+ class User < ApplicationRecord
1007
+ has_active_fields scope_method: :tenant_and_department_scope
1008
+
1009
+ # The scope method should return a string
1010
+ def tenant_and_department_scope
1011
+ "#{tenant_id}-#{department_id}"
1012
+ end
1013
+
1014
+ after_update :clear_unavailable_active_values, if: :tenant_and_department_scope_changed?
1015
+
1016
+ private
1017
+
1018
+ def tenant_and_department_scope_changed?
1019
+ saved_change_to_tenant_id? || saved_change_to_department_id?
1020
+ end
1021
+ end
1022
+ ```
1023
+
852
1024
  ### Localization (I18n)
853
1025
 
854
1026
  The built-in _validators_ primarily use _Rails_ default error types.
@@ -861,9 +1033,7 @@ For an example, refer to the [locale file](https://github.com/lassoid/active_fie
861
1033
 
862
1034
  ## Current Restrictions
863
1035
 
864
- 1. Only _PostgreSQL_ 17+ is fully supported.
865
-
866
- The gem is tested exclusively with _PostgreSQL_. Support for other databases is not guaranteed.
1036
+ 1. This gem requires _PostgreSQL_ and is not designed to support other database systems.
867
1037
 
868
1038
  2. Updating some _Active Fields_ options may be unsafe.
869
1039
 
@@ -901,6 +1071,7 @@ active_field.available_customizable_types # Available Customizable types for thi
901
1071
 
902
1072
  # Scopes:
903
1073
  ActiveFields::Field::Boolean.for("Post") # Collection of Active Fields registered for the specified Customizable type
1074
+ ActiveFields::Field::Integer.for("User", scope: "main_tenant") # Collection of Active Fields available for the specified Customizable type with given scope
904
1075
  ```
905
1076
 
906
1077
  ### Values API
@@ -929,10 +1100,13 @@ customizable = Post.take
929
1100
  customizable.active_values # `has_many` association with Active Values linked to this Customizable
930
1101
 
931
1102
  # Methods:
932
- customizable.active_fields # Collection of Active Fields registered for this record
933
- Post.active_fields # Collection of Active Fields registered for this model
1103
+ customizable.active_fields # Collection of Active Fields available for this record
1104
+ customizable.active_fields_scope # Scope value for this record
1105
+ Post.active_fields # Collection of Active Fields available for this model
1106
+ User.active_fields(scope: "main_tenant") # Collection of Active Fields available for this model and given scope
934
1107
  Post.allowed_active_fields_type_names # Active Fields type names allowed for this Customizable model
935
1108
  Post.allowed_active_fields_class_names # Active Fields class names allowed for this Customizable model
1109
+ User.active_fields_scope_method # Scope method for this model
936
1110
 
937
1111
  # Create, update or destroy Active Values.
938
1112
  customizable.active_fields_attributes = [
@@ -963,6 +1137,10 @@ customizable.active_values_attributes = attributes
963
1137
  # `form.fields_for :active_fields, customizable.initialize_active_values`.
964
1138
  customizable.initialize_active_values
965
1139
 
1140
+ # Destroys Active Values that are no longer associated with Active Fields available for this record.
1141
+ # Call this method after changing the scope value to ensure all Active Values are valid.
1142
+ customizable.clear_unavailable_active_values
1143
+
966
1144
  # Query Customizables by Active Values.
967
1145
  Post.where_active_fields(
968
1146
  [
@@ -971,6 +1149,11 @@ Post.where_active_fields(
971
1149
  { n: "boolean", op: "!=", v: false }, # compact form (string or symbol keys)
972
1150
  ],
973
1151
  )
1152
+ # Search with given scope.
1153
+ User.where_active_fields(
1154
+ filters,
1155
+ scope: "main_tenant",
1156
+ )
974
1157
  ```
975
1158
 
976
1159
  ### Global Config
@@ -1013,14 +1196,7 @@ and push the `.gem` file to [rubygems.org](https://rubygems.org).
1013
1196
  ## Contributing
1014
1197
 
1015
1198
  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
1199
 
1019
1200
  ## License
1020
1201
 
1021
1202
  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
@@ -5,6 +5,4 @@ require "bundler/setup"
5
5
  APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
6
6
  load "rails/tasks/engine.rake"
7
7
 
8
- load "rails/tasks/statistics.rake"
9
-
10
8
  require "bundler/gem_tasks"
@@ -23,6 +23,10 @@ module ActiveFields
23
23
  # - <tt>:op</tt> or <tt>:operator</tt> key specifying search operation or operator;
24
24
  # - <tt>:v</tt> or <tt>:value</tt> key specifying search value.
25
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
+ #
26
30
  # Example:
27
31
  #
28
32
  # # Array of hashes
@@ -45,7 +49,13 @@ module ActiveFields
45
49
  #
46
50
  # # Params (must be permitted)
47
51
  # CustomizableModel.where_active_fields(permitted_params)
48
- scope :where_active_fields, ->(filters) do
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
49
59
  filters = filters.to_h if filters.respond_to?(:permitted?)
50
60
 
51
61
  unless filters.is_a?(Array) || filters.is_a?(Hash)
@@ -55,23 +65,27 @@ module ActiveFields
55
65
  # Handle `fields_for` params
56
66
  filters = filters.values if filters.is_a?(Hash)
57
67
 
58
- active_fields_by_name = active_fields.index_by(&:name)
68
+ active_fields_by_name = active_fields(scope: scope).index_by(&:name)
59
69
 
60
- filters.inject(self) do |scope, filter|
70
+ filters.inject(self) do |query, filter|
61
71
  filter = filter.to_h if filter.respond_to?(:permitted?)
62
72
  filter = filter.with_indifferent_access
63
73
 
64
- active_field = active_fields_by_name[filter[:n] || filter[:name]]
65
- next scope if active_field.nil?
66
- next scope if active_field.value_finder.nil?
74
+ name = filter.key?(:n) ? filter[:n] : filter[:name]
75
+ operator = filter.key?(:op) ? filter[:op] : filter[:operator]
76
+ value = filter.key?(:v) ? filter[:v] : filter[:value]
77
+
78
+ active_field = active_fields_by_name[name]
79
+ next query if active_field.nil?
80
+ next query if active_field.value_finder.nil?
67
81
 
68
82
  active_values = active_field.value_finder.search(
69
- op: filter[:op] || filter[:operator],
70
- value: filter[:v] || filter[:value],
83
+ op: operator,
84
+ value: value,
71
85
  )
72
- next scope if active_values.nil?
86
+ next query if active_values.nil?
73
87
 
74
- scope.where(id: active_values.select(:customizable_id))
88
+ query.where(id: active_values.select(:customizable_id))
75
89
  end
76
90
  end
77
91
 
@@ -79,9 +93,13 @@ module ActiveFields
79
93
  end
80
94
 
81
95
  class_methods do
82
- # Collection of active fields registered for this customizable
83
- def active_fields
84
- ActiveFields.config.field_base_class.for(name)
96
+ # Returns the collection of active fields associated with this customizable model.
97
+ # You can provide an optional <tt>:scope</tt> parameter
98
+ # to retrieve active fields available for a specific scope, not just global fields.
99
+ def active_fields(scope: nil)
100
+ scope = nil if active_fields_scope_method.nil?
101
+
102
+ ActiveFields.config.field_base_class.for(name, scope: scope)
85
103
  end
86
104
 
87
105
  # Returns active fields type names allowed for this customizable model.
@@ -95,7 +113,17 @@ module ActiveFields
95
113
  end
96
114
  end
97
115
 
98
- delegate :active_fields, to: :class
116
+ # Collection of active fields registered for this customizable.
117
+ def active_fields
118
+ self.class.active_fields(scope: active_fields_scope)
119
+ end
120
+
121
+ # Scope value for the active fields collection.
122
+ def active_fields_scope
123
+ return if self.class.active_fields_scope_method.nil?
124
+
125
+ send(self.class.active_fields_scope_method)&.to_s
126
+ end
99
127
 
100
128
  # Assigns the given attributes to the active_values association.
101
129
  #
@@ -177,5 +205,29 @@ module ActiveFields
177
205
 
178
206
  active_values
179
207
  end
208
+
209
+ # Destroys all active_values that are no longer available for this customizable
210
+ # (e.g. after its scope has changed).
211
+ #
212
+ # This method is suitable for customizables with scope functionality enabled
213
+ # (when `has_active_fields` is called with a `scope_method:` parameter).
214
+ # When a customizable's scope value changes,
215
+ # some active_values may reference active_fields that are no longer available for the new scope.
216
+ # This method identifies and destroys those orphaned active_values.
217
+ #
218
+ # Example:
219
+ #
220
+ # class User < ApplicationRecord
221
+ # has_active_fields scope_method: :tenant_id
222
+ #
223
+ # after_update :clear_unavailable_active_values, if: :saved_change_to_tenant_id?
224
+ # end
225
+ def clear_unavailable_active_values
226
+ available_field_ids = active_fields.pluck(:id)
227
+
228
+ active_values.select do |active_value|
229
+ available_field_ids.exclude?(active_value.active_field_id)
230
+ end.each(&:destroy)
231
+ end
180
232
  end
181
233
  end