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.
- checksums.yaml +4 -4
- data/.rubocop.yml +15 -2
- data/Appraisals +19 -0
- data/CHANGELOG.md +9 -1
- data/README.md +202 -26
- data/Rakefile +0 -2
- data/app/models/concerns/active_fields/customizable_concern.rb +66 -14
- data/app/models/concerns/active_fields/field_concern.rb +38 -4
- data/app/models/concerns/active_fields/value_concern.rb +8 -1
- 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/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: 86d3c04e24cae1b29e2f3a4957cec89fe64132c6b3d7f5432f16c6a84a0d8914
|
|
4
|
+
data.tar.gz: d1b6d2385b47913edf57ca4709e73eaf04f64f7d6d87a3c5e028b092f3b1a97e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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,6 +1,14 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
-
|
|
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
|
[](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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
933
|
-
|
|
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
|
@@ -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
|
-
|
|
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 |
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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:
|
|
70
|
-
value:
|
|
83
|
+
op: operator,
|
|
84
|
+
value: value,
|
|
71
85
|
)
|
|
72
|
-
next
|
|
86
|
+
next query if active_values.nil?
|
|
73
87
|
|
|
74
|
-
|
|
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
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|