activeadmin_favorites 0.1.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 +7 -0
- data/CHANGELOG.md +17 -0
- data/CODE_OF_CONDUCT.md +31 -0
- data/CONTRIBUTING.md +29 -0
- data/GOVERNANCE.md +29 -0
- data/LICENSE.md +21 -0
- data/README.md +146 -0
- data/SECURITY.md +27 -0
- data/activeadmin_favorites.gemspec +68 -0
- data/app/assets/controllers/activeadmin_favorites/personalization_controller.js +199 -0
- data/app/models/active_admin/favorites/application_record.rb +9 -0
- data/app/models/active_admin/favorites/combined_favorite.rb +9 -0
- data/app/models/active_admin/favorites/favorite/macro_builder.rb +43 -0
- data/app/models/active_admin/favorites/favorite/macro_catalog.rb +118 -0
- data/app/models/active_admin/favorites/favorite/macro_resolver.rb +86 -0
- data/app/models/active_admin/favorites/favorite/macro_validator.rb +87 -0
- data/app/models/active_admin/favorites/favorite/query_params.rb +41 -0
- data/app/models/active_admin/favorites/favorite/range_detector.rb +54 -0
- data/app/models/active_admin/favorites/favorite.rb +339 -0
- data/app/models/active_admin/favorites/filters_favorite.rb +8 -0
- data/app/models/active_admin/favorites/lens_favorite.rb +9 -0
- data/app/models/active_admin/favorites/view_lens/layout.rb +86 -0
- data/app/models/active_admin/favorites/view_lens_default.rb +40 -0
- data/app/models/active_admin/favorites.rb +6 -0
- data/app/views/active_admin/favorites/_lens_catalog_data.html.erb +9 -0
- data/app/views/active_admin/favorites/_macro_fields.html.erb +53 -0
- data/app/views/active_admin/favorites/_personalization_trigger.html.erb +69 -0
- data/app/views/active_admin/resource/index.html.arb +99 -0
- data/app/views/active_admin/resource/show.html.arb +13 -0
- data/config/locales/activeadmin_favorites.en.yml +60 -0
- data/config/locales/activeadmin_favorites.host.yml.example +17 -0
- data/config/polyrun_coverage.yml +8 -0
- data/db/migrate/20260709100000_create_active_admin_view_lens_defaults.rb +24 -0
- data/db/migrate/20260709130000_create_admin_favorites.rb +33 -0
- data/lib/activeadmin/favorites/configuration.rb +31 -0
- data/lib/activeadmin/favorites/engine.rb +75 -0
- data/lib/activeadmin/favorites/hash_coercion.rb +34 -0
- data/lib/activeadmin/favorites/install.rb +121 -0
- data/lib/activeadmin/favorites/migration_helpers.rb +34 -0
- data/lib/activeadmin/favorites/migration_support.rb +19 -0
- data/lib/activeadmin/favorites/register_favorites.rb +272 -0
- data/lib/activeadmin/favorites/register_view_lens_preferences.rb +98 -0
- data/lib/activeadmin/favorites/user_record.rb +26 -0
- data/lib/activeadmin/favorites/version.rb +7 -0
- data/lib/activeadmin/favorites/view_lens/applicator.rb +150 -0
- data/lib/activeadmin/favorites/view_lens/catalog.rb +108 -0
- data/lib/activeadmin/favorites/view_lens/current.rb +28 -0
- data/lib/activeadmin/favorites/view_lens/patches.rb +95 -0
- data/lib/activeadmin/favorites/view_lens/resource_dsl.rb +44 -0
- data/lib/activeadmin/favorites/view_lens.rb +8 -0
- data/lib/activeadmin/favorites.rb +30 -0
- data/lib/activeadmin_favorites.rb +5 -0
- data/lib/generators/activeadmin_favorites/install/templates/favorite_policy.rb +56 -0
- data/lib/generators/activeadmin_favorites/install/templates/initializer.rb +16 -0
- data/lib/generators/activeadmin_favorites/install/templates/locale.en.yml +60 -0
- data/lib/generators/activeadmin_favorites/install_generator.rb +37 -0
- data/lib/tasks/activeadmin_favorites_tasks.rake +39 -0
- data/sig/activeadmin/favorites.rbs +5 -0
- metadata +418 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 51027c5b7293ba07e01be5677e4785b9d682c177ef6cea6137b4f658cb2ede71
|
|
4
|
+
data.tar.gz: 24bbfd1df143374ccd612f14e6bec75967a44571ad58013d1203ca03c44c02be
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e62dc3e8a67dcf78d388d00764eef5f9e4051d7ae446680d5c28775eebd6b3efb5c09443b19a0cf64e2333bdb4acfe03fbae62cc6021ec4b84d19287b1cb11ea
|
|
7
|
+
data.tar.gz: 51fad41e3d2841c6d4d55758e33e62e74539c96dc27035dcac0ddf5de2568a197da9b921aee3f90467e2233ca4554adc23659765acb166a9d3696130c11a1785
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- CI: run Rails 7.2 appraisal on Ruby 3.4; refresh lockfiles for current advisories (`loofah` 2.25.2, `rails-html-sanitizer` 1.7.1) and development gems (`rbs` ~> 4).
|
|
6
|
+
|
|
7
|
+
## 0.1.0 (2026-07-13)
|
|
8
|
+
|
|
9
|
+
- Initial public release: saved filter favorites and view lens personalization for ActiveAdmin 4.
|
|
10
|
+
- STI storage on configurable `admin_favorites` (`FiltersFavorite`, `LensFavorite`, `CombinedFavorite`).
|
|
11
|
+
- ActiveAdmin resource at `/admin/favorites`.
|
|
12
|
+
- Request-scoped `ViewLens::Applicator` state via `ActiveSupport::CurrentAttributes`.
|
|
13
|
+
- Deferred catalog JSON for cogwheel column and panel toggles after table render.
|
|
14
|
+
- Configurable user association via `UserRecord` (`user_id` defaults).
|
|
15
|
+
- Engine migration append with host-version deduplication.
|
|
16
|
+
- `rails activeadmin_favorites:install` generator and `activeadmin_favorites:install:migrations` rake task.
|
|
17
|
+
- English locale; host locale template for copy into additional languages.
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our pledge
|
|
4
|
+
|
|
5
|
+
We pledge to make participation in our community a harassment-free experience for everyone. We operate on principles of mutual respect, privacy, and authentic engagement. We value substantive contributions and clarity on intentions.
|
|
6
|
+
|
|
7
|
+
## Our standards
|
|
8
|
+
|
|
9
|
+
Examples of behavior that contributes to a positive environment include:
|
|
10
|
+
|
|
11
|
+
- Authenticity: engaging with genuine curiosity and admitting uncertainty rather than feigning knowledge.
|
|
12
|
+
- Responsible innovation: taking full responsibility for any content or code contributed, whether manually written or generated by automation tools.
|
|
13
|
+
- Gentle correction: responding politely to errors. We view mistakes as opportunities for learning, provided they are addressed with humility.
|
|
14
|
+
- Inclusive language: using language that welcomes diverse perspectives and respects the privacy and identity of all participants.
|
|
15
|
+
|
|
16
|
+
Examples of unacceptable behavior include:
|
|
17
|
+
|
|
18
|
+
- Harassment: public or private harassment, trolling, or insulting comments.
|
|
19
|
+
- Weaponized complexity: using jargon or overwhelming volume (including automated spam) to silence others.
|
|
20
|
+
- Publishing private information: sharing others' data or personal context without explicit permission.
|
|
21
|
+
|
|
22
|
+
## Artificial intelligence and automation
|
|
23
|
+
|
|
24
|
+
In accordance with our commitment to collective awareness:
|
|
25
|
+
|
|
26
|
+
- Contributors are responsible for the accuracy and security of any AI-generated artifacts they submit.
|
|
27
|
+
- "The AI wrote it" is not a valid excuse for introducing bugs, security vulnerabilities, or bias.
|
|
28
|
+
|
|
29
|
+
## Enforcement
|
|
30
|
+
|
|
31
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@kiskolabs.com. All complaints will be reviewed and investigated promptly and fairly.
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Contributing Guidelines
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing to activeadmin_favorites. We value learning over perfection but require rigor and responsibility.
|
|
4
|
+
|
|
5
|
+
## The golden rule of automation
|
|
6
|
+
|
|
7
|
+
We welcome the use of AI and automation tools to reduce toil, but you must strictly adhere to the following:
|
|
8
|
+
|
|
9
|
+
1. You are the author. You act as the responsible agent for any code you submit. You must review, debug, and understand every line.
|
|
10
|
+
2. Manage cognitive load. Do not submit massive, unreviewed automated dumps. Respect the reviewers' time by annotating complex logic.
|
|
11
|
+
3. Security. Never feed project secrets or private context into public AI models.
|
|
12
|
+
|
|
13
|
+
## How to contribute
|
|
14
|
+
|
|
15
|
+
### Reporting issues
|
|
16
|
+
|
|
17
|
+
- Verify accuracy before posting. Avoid generalizations.
|
|
18
|
+
- Use issue templates to provide clear goals, constraints, and reproduction steps.
|
|
19
|
+
|
|
20
|
+
### Pull request process
|
|
21
|
+
|
|
22
|
+
- Scope: keep PRs focused on a single goal.
|
|
23
|
+
- Context: explain why the change is necessary.
|
|
24
|
+
- Testing: run `make test` locally before opening a PR.
|
|
25
|
+
|
|
26
|
+
### Review process
|
|
27
|
+
|
|
28
|
+
- We encourage productive friction. Expect questions about your approach.
|
|
29
|
+
- If a reviewer suggests a change, view it as mutual aid, not criticism.
|
data/GOVERNANCE.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Project Governance
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This project balances individual autonomy with collective coordination. We aim for a workflow that supports rapid iteration while maintaining strict guardrails for safety and architectural integrity.
|
|
6
|
+
|
|
7
|
+
## Roles
|
|
8
|
+
|
|
9
|
+
### Contributors
|
|
10
|
+
|
|
11
|
+
Anyone who submits code, documentation, or participates in discussions. Contributors are expected to exercise direct action—taking ownership of problems they identify—while adhering to our quality standards.
|
|
12
|
+
|
|
13
|
+
### Maintainers (Human Oversight)
|
|
14
|
+
|
|
15
|
+
Maintainers are responsible for:
|
|
16
|
+
|
|
17
|
+
1. Strategic judgment: defining scope and architectural direction.
|
|
18
|
+
2. Review: verifying that contributions (human or automated) meet security and logic standards.
|
|
19
|
+
3. Consensus building: facilitating decisions when the community is divided.
|
|
20
|
+
|
|
21
|
+
## Decision making process
|
|
22
|
+
|
|
23
|
+
### Lazy consensus
|
|
24
|
+
|
|
25
|
+
For most routine changes, we operate on lazy consensus. If a proposal is made and no objections are raised within two weeks, it is considered approved.
|
|
26
|
+
|
|
27
|
+
### Strategic alignment
|
|
28
|
+
|
|
29
|
+
Major architectural changes, high-risk automation integrations, or changes that affect business logic require explicit approval from the maintainers. We prioritize substance over performance—a change must solve a real problem, not just appear polished.
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 amkisko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# activeadmin_favorites
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/activeadmin_favorites) [](https://github.com/amkisko/activeadmin_favorites.rb/actions/workflows/test.yml)
|
|
4
|
+
|
|
5
|
+
Saved filter favorites, per-resource view lens defaults, and a cogwheel personalization UI for ActiveAdmin 4 index and show pages.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Ruby 3.2+ (CI matrix: Ruby 3.4, 4.0, and TruffleRuby)
|
|
10
|
+
- Rails 7.2+ (Rails 7.2 and 8.1 tested in CI)
|
|
11
|
+
- ActiveAdmin 4.0.0.beta13+
|
|
12
|
+
- PostgreSQL for production migrations (`jsonb` columns and concurrent index helpers)
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
Add the gem to your Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem "activeadmin_favorites"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Run the install generator:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bin/rails generate activeadmin_favorites:install
|
|
26
|
+
bin/rails db:migrate
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or copy migrations manually:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bin/rails activeadmin_favorites:install:migrations
|
|
33
|
+
bin/rails db:migrate
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Greenfield apps pick up engine migrations automatically when no matching version exists in `db/migrate/`.
|
|
37
|
+
|
|
38
|
+
Configure the host app (the generator writes `config/initializers/activeadmin_favorites.rb` with neutral defaults):
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
ActiveAdmin::Favorites.configure do |config|
|
|
42
|
+
config.user_class = "User"
|
|
43
|
+
config.user_foreign_key = :user_id
|
|
44
|
+
config.user_association_name = :user
|
|
45
|
+
config.user_label_method = :email
|
|
46
|
+
config.current_user_method = :current_user
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Add `ActiveAdmin::Favorites::FavoritePolicy` in the host app (the generator copies a template). Point `has_many :favorites` on your user model at `ActiveAdmin::Favorites::Favorite`.
|
|
51
|
+
|
|
52
|
+
Configure the table name when needed:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
ActiveAdmin::Favorites.configure do |config|
|
|
56
|
+
config.table_name = "admin_favorites"
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
When using ActionPolicy, add `save_lens_favorite?` and `reset?` to `ActiveAdmin::PagePolicy` (or equivalent) so the view lens preference page actions authorize correctly.
|
|
61
|
+
|
|
62
|
+
### Locales
|
|
63
|
+
|
|
64
|
+
English ships with the gem. Copy `config/locales/activeadmin_favorites.host.yml.example` into your locale files and translate `active_admin.favorites` keys.
|
|
65
|
+
|
|
66
|
+
## Favorite kinds
|
|
67
|
+
|
|
68
|
+
| Kind | Stores |
|
|
69
|
+
|------|--------|
|
|
70
|
+
| `filters` | scope, sort, ransack query, macros |
|
|
71
|
+
| `lens` | hidden columns, filters, panels, rows, action items |
|
|
72
|
+
| `combined` | filters and layout together |
|
|
73
|
+
|
|
74
|
+
## View lens layout schema
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"version": 1,
|
|
79
|
+
"hidden": {
|
|
80
|
+
"columns": ["created_at"],
|
|
81
|
+
"filters": ["status"],
|
|
82
|
+
"action_items": ["new"],
|
|
83
|
+
"panels": ["details"],
|
|
84
|
+
"show_rows": ["author"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Version 1 supports show/hide only. Column reorder is deferred.
|
|
90
|
+
|
|
91
|
+
Catalog entries are discovered at render time from ActiveAdmin columns, filters, panels, and show rows. Use `lens_id:` on custom columns or panels when auto ids are unstable. Block columns without a title receive stable `block_column_N` ids automatically. Personalization catalog data is injected on every index layout, not only table indexes.
|
|
92
|
+
|
|
93
|
+
## Personalization UI
|
|
94
|
+
|
|
95
|
+
The gem installs:
|
|
96
|
+
|
|
97
|
+
- **Add to favorites** on index pages (saved filters)
|
|
98
|
+
- **Personalize page** cogwheel on index and show pages
|
|
99
|
+
- **Favorites** hub with type tabs (all, filters, lens, combined)
|
|
100
|
+
|
|
101
|
+
Defaults are stored in `active_admin_view_lens_defaults`. Named lens favorites and combined favorites use `admin_favorites.layout`.
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
bundle install
|
|
107
|
+
bundle exec appraisal install
|
|
108
|
+
make lint
|
|
109
|
+
make test
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
See `spec/README.md` for focused runs and coverage notes.
|
|
113
|
+
|
|
114
|
+
If native extensions fail to load after switching Ruby installs (for example mise vs rbenv with the same version), wipe and reinstall both bundle trees with one active Ruby:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
rm -rf vendor/bundle
|
|
118
|
+
bundle install
|
|
119
|
+
|
|
120
|
+
rm -rf gemfiles/vendor/bundle
|
|
121
|
+
bundle exec appraisal install
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Links
|
|
125
|
+
|
|
126
|
+
- [GitHub](https://github.com/amkisko/activeadmin_favorites.rb)
|
|
127
|
+
- [GitLab](https://gitlab.com/amkisko/activeadmin_favorites.rb)
|
|
128
|
+
- [RubyGems](https://rubygems.org/gems/activeadmin_favorites)
|
|
129
|
+
- [libraries.io](https://libraries.io/rubygems/activeadmin_favorites)
|
|
130
|
+
- [Deps.dev](https://deps.dev/rubygems/activeadmin_favorites)
|
|
131
|
+
- [SonarCloud](https://sonarcloud.io/project/overview?id=amkisko_activeadmin_favorites.rb)
|
|
132
|
+
- [Snyk](https://snyk.io/test/github/amkisko/activeadmin_favorites.rb)
|
|
133
|
+
- [Codecov](https://app.codecov.io/github/amkisko/activeadmin_favorites.rb)
|
|
134
|
+
- [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/amkisko/activeadmin_favorites.rb)
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
139
|
+
|
|
140
|
+
## Sponsors
|
|
141
|
+
|
|
142
|
+
Sponsored by [Kisko Labs](https://www.kiskolabs.com).
|
|
143
|
+
|
|
144
|
+
<a href="https://www.kiskolabs.com">
|
|
145
|
+
<img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
|
|
146
|
+
</a>
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# SECURITY
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Do NOT open a public GitHub issue for security vulnerabilities.
|
|
6
|
+
|
|
7
|
+
Email security details to: security@kiskolabs.com
|
|
8
|
+
|
|
9
|
+
Include: description, steps to reproduce, potential impact, and suggested fix (if available).
|
|
10
|
+
|
|
11
|
+
### Response timeline
|
|
12
|
+
|
|
13
|
+
- We will acknowledge receipt of your report
|
|
14
|
+
- We will provide an initial assessment
|
|
15
|
+
- We will keep you informed of our progress and resolution timeline
|
|
16
|
+
|
|
17
|
+
### Disclosure policy
|
|
18
|
+
|
|
19
|
+
- We will work with you to understand and resolve the issue
|
|
20
|
+
- We will credit you for the discovery (unless you prefer to remain anonymous)
|
|
21
|
+
- We will publish a security advisory after the vulnerability is patched
|
|
22
|
+
- We will coordinate public disclosure with you
|
|
23
|
+
|
|
24
|
+
## Automation security
|
|
25
|
+
|
|
26
|
+
- Context isolation: it is strictly forbidden to include production credentials, API keys, or personally identifiable information in prompts sent to third-party LLMs or automation services.
|
|
27
|
+
- Supply chain: all automated dependencies must be verified.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/activeadmin/favorites/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "activeadmin_favorites"
|
|
7
|
+
spec.version = ActiveAdmin::Favorites::VERSION
|
|
8
|
+
spec.authors = ["Andrei Makarov"]
|
|
9
|
+
spec.email = ["contact@kiskolabs.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Favorites and view lens personalization for ActiveAdmin 4"
|
|
12
|
+
spec.description = "Saved filter bookmarks, per-resource layout preferences, and a cogwheel personalization UI for ActiveAdmin index and show pages."
|
|
13
|
+
spec.license = "MIT"
|
|
14
|
+
spec.platform = Gem::Platform::RUBY
|
|
15
|
+
spec.required_ruby_version = ">= 3.4"
|
|
16
|
+
|
|
17
|
+
repository_url = "https://github.com/amkisko/activeadmin_favorites.rb"
|
|
18
|
+
|
|
19
|
+
spec.homepage = repository_url
|
|
20
|
+
spec.metadata = {
|
|
21
|
+
"homepage_uri" => repository_url,
|
|
22
|
+
"source_code_uri" => "#{repository_url}/tree/main",
|
|
23
|
+
"changelog_uri" => "#{repository_url}/blob/main/CHANGELOG.md",
|
|
24
|
+
"bug_tracker_uri" => "#{repository_url}/issues",
|
|
25
|
+
"documentation_uri" => "#{repository_url}#readme",
|
|
26
|
+
"rubygems_mfa_required" => "true"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
spec.files = Dir.chdir(__dir__) do
|
|
30
|
+
%w[
|
|
31
|
+
CHANGELOG.md
|
|
32
|
+
CODE_OF_CONDUCT.md
|
|
33
|
+
CONTRIBUTING.md
|
|
34
|
+
GOVERNANCE.md
|
|
35
|
+
LICENSE.md
|
|
36
|
+
README.md
|
|
37
|
+
SECURITY.md
|
|
38
|
+
activeadmin_favorites.gemspec
|
|
39
|
+
].select { |path| File.file?(path) } +
|
|
40
|
+
Dir["{app,config,db,lib,sig}/**/*"]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
spec.require_paths = ["lib"]
|
|
44
|
+
|
|
45
|
+
spec.add_dependency "activeadmin", ">= 4.0.0.beta13", "< 5"
|
|
46
|
+
spec.add_dependency "importmap-rails"
|
|
47
|
+
spec.add_dependency "rails", ">= 7.2"
|
|
48
|
+
|
|
49
|
+
spec.add_development_dependency "factory_bot", "~> 6"
|
|
50
|
+
spec.add_development_dependency "appraisal", "~> 2"
|
|
51
|
+
spec.add_development_dependency "bundler-audit", "~> 0.9"
|
|
52
|
+
spec.add_development_dependency "bundler", ">= 2"
|
|
53
|
+
spec.add_development_dependency "polyrun", ">= 2.2.0"
|
|
54
|
+
spec.add_development_dependency "prosopite", "~> 2.0"
|
|
55
|
+
spec.add_development_dependency "rake", "~> 13"
|
|
56
|
+
spec.add_development_dependency "rspec", "~> 3"
|
|
57
|
+
spec.add_development_dependency "rspec-rails", ">= 6"
|
|
58
|
+
spec.add_development_dependency "sqlite3", ">= 1"
|
|
59
|
+
spec.add_development_dependency "rubocop-rails", "~> 2.34"
|
|
60
|
+
spec.add_development_dependency "rubocop-rspec", "~> 3.8"
|
|
61
|
+
spec.add_development_dependency "rubocop-thread_safety", "~> 0.7"
|
|
62
|
+
spec.add_development_dependency "standard", "~> 1.52"
|
|
63
|
+
spec.add_development_dependency "standard-custom", "~> 1.0"
|
|
64
|
+
spec.add_development_dependency "standard-performance", "~> 1.8"
|
|
65
|
+
spec.add_development_dependency "standard-rails", "~> 1.5"
|
|
66
|
+
spec.add_development_dependency "standard-rspec", "~> 0.3"
|
|
67
|
+
spec.add_development_dependency "rbs", "~> 4"
|
|
68
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["menu", "catalogGroups", "visibilityCheckbox"]
|
|
5
|
+
static values = {
|
|
6
|
+
resourceKey: String,
|
|
7
|
+
actionName: String,
|
|
8
|
+
path: String,
|
|
9
|
+
catalogElementId: String,
|
|
10
|
+
hidden: Object,
|
|
11
|
+
groupLabels: Object,
|
|
12
|
+
emptyCatalogLabel: String,
|
|
13
|
+
updateUrl: String,
|
|
14
|
+
resetUrl: String,
|
|
15
|
+
saveLensUrl: String,
|
|
16
|
+
favoritesUrl: String
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
connect() {
|
|
20
|
+
this.catalogLoaded = false
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
prepareMenu() {
|
|
24
|
+
if (!this.catalogLoaded) {
|
|
25
|
+
this.renderCatalogGroups()
|
|
26
|
+
this.catalogLoaded = true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.syncCheckboxesFromHidden()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
catalogEntries() {
|
|
33
|
+
const element = document.getElementById(this.catalogElementIdValue)
|
|
34
|
+
if (!element) return []
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(element.textContent)
|
|
38
|
+
} catch (_error) {
|
|
39
|
+
return []
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
escapeHtml(value) {
|
|
44
|
+
return String(value)
|
|
45
|
+
.replaceAll("&", "&")
|
|
46
|
+
.replaceAll("<", "<")
|
|
47
|
+
.replaceAll(">", ">")
|
|
48
|
+
.replaceAll('"', """)
|
|
49
|
+
.replaceAll("'", "'")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
renderCatalogGroups() {
|
|
53
|
+
const grouped = this.catalogEntries().reduce((result, entry) => {
|
|
54
|
+
result[entry.group] ||= []
|
|
55
|
+
result[entry.group].push(entry)
|
|
56
|
+
return result
|
|
57
|
+
}, {})
|
|
58
|
+
|
|
59
|
+
const groups = Object.keys(grouped)
|
|
60
|
+
if (groups.length === 0) {
|
|
61
|
+
this.catalogGroupsTarget.innerHTML = `<p class="text-sm text-gray-600 dark:text-gray-300">${this.escapeHtml(this.emptyCatalogLabelValue)}</p>`
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.catalogGroupsTarget.innerHTML = groups.map((group) => {
|
|
66
|
+
const label = this.escapeHtml(this.groupLabelsValue[group] || group.replace(/_/g, " "))
|
|
67
|
+
const items = grouped[group].map((entry) => {
|
|
68
|
+
const entryLabel = this.escapeHtml(entry.label)
|
|
69
|
+
const entryGroup = this.escapeHtml(entry.group)
|
|
70
|
+
const entryId = this.escapeHtml(entry.id)
|
|
71
|
+
return `
|
|
72
|
+
<li>
|
|
73
|
+
<label class="inline-flex items-center gap-2 text-sm">
|
|
74
|
+
<input
|
|
75
|
+
type="checkbox"
|
|
76
|
+
checked
|
|
77
|
+
class="rounded border-gray-300 dark:border-white/20"
|
|
78
|
+
data-group="${entryGroup}"
|
|
79
|
+
data-entry-id="${entryId}"
|
|
80
|
+
data-${this.identifier}-target="visibilityCheckbox"
|
|
81
|
+
/>
|
|
82
|
+
<span>${entryLabel}</span>
|
|
83
|
+
</label>
|
|
84
|
+
</li>
|
|
85
|
+
`
|
|
86
|
+
}).join("")
|
|
87
|
+
|
|
88
|
+
return `
|
|
89
|
+
<div class="mb-4">
|
|
90
|
+
<h4 class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-2">${label}</h4>
|
|
91
|
+
<ul class="space-y-2 max-h-40 overflow-y-auto">${items}</ul>
|
|
92
|
+
</div>
|
|
93
|
+
`
|
|
94
|
+
}).join("")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
syncCheckboxesFromHidden() {
|
|
98
|
+
const hidden = this.hiddenValue || {}
|
|
99
|
+
this.visibilityCheckboxTargets.forEach((checkbox) => {
|
|
100
|
+
const group = checkbox.dataset.group
|
|
101
|
+
const entryId = checkbox.dataset.entryId
|
|
102
|
+
const hiddenIds = hidden[group] || []
|
|
103
|
+
checkbox.checked = !hiddenIds.includes(entryId)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
currentLayout() {
|
|
108
|
+
const hidden = {}
|
|
109
|
+
this.visibilityCheckboxTargets.forEach((checkbox) => {
|
|
110
|
+
const group = checkbox.dataset.group
|
|
111
|
+
const entryId = checkbox.dataset.entryId
|
|
112
|
+
if (!checkbox.checked) {
|
|
113
|
+
hidden[group] ||= []
|
|
114
|
+
hidden[group].push(entryId)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
version: 1,
|
|
120
|
+
hidden
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async saveDefault() {
|
|
125
|
+
await this.submitLayout(this.updateUrlValue, "PATCH")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async resetDefault() {
|
|
129
|
+
const body = new FormData()
|
|
130
|
+
body.append("resource_key", this.resourceKeyValue)
|
|
131
|
+
body.append("view_lens_action", this.actionNameValue)
|
|
132
|
+
body.append("_method", "delete")
|
|
133
|
+
body.append("authenticity_token", this.csrfToken())
|
|
134
|
+
|
|
135
|
+
const response = await fetch(this.resetUrlValue, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
body,
|
|
138
|
+
headers: { Accept: "text/html" }
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if (response.redirected) {
|
|
142
|
+
window.location.href = response.url
|
|
143
|
+
} else if (response.ok) {
|
|
144
|
+
window.location.reload()
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async saveLensFavorite() {
|
|
149
|
+
const name = window.prompt(this.promptLabel())
|
|
150
|
+
if (!name) return
|
|
151
|
+
|
|
152
|
+
const body = new FormData()
|
|
153
|
+
body.append("name", name)
|
|
154
|
+
body.append("resource_key", this.resourceKeyValue)
|
|
155
|
+
body.append("view_lens_action", this.actionNameValue)
|
|
156
|
+
body.append("path", this.pathValue)
|
|
157
|
+
body.append("layout", JSON.stringify(this.currentLayout()))
|
|
158
|
+
body.append("authenticity_token", this.csrfToken())
|
|
159
|
+
|
|
160
|
+
const response = await fetch(this.saveLensUrlValue, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
body,
|
|
163
|
+
headers: { Accept: "text/html" }
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
if (response.redirected) {
|
|
167
|
+
window.location.href = response.url
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async submitLayout(url, method) {
|
|
172
|
+
const body = new FormData()
|
|
173
|
+
body.append("resource_key", this.resourceKeyValue)
|
|
174
|
+
body.append("view_lens_action", this.actionNameValue)
|
|
175
|
+
body.append("layout", JSON.stringify(this.currentLayout()))
|
|
176
|
+
body.append("authenticity_token", this.csrfToken())
|
|
177
|
+
if (method !== "POST") body.append("_method", method.toLowerCase())
|
|
178
|
+
|
|
179
|
+
const response = await fetch(url, {
|
|
180
|
+
method: method === "PATCH" ? "POST" : method,
|
|
181
|
+
body,
|
|
182
|
+
headers: { Accept: "text/html" }
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
if (response.redirected) {
|
|
186
|
+
window.location.href = response.url
|
|
187
|
+
} else if (response.ok) {
|
|
188
|
+
window.location.reload()
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
csrfToken() {
|
|
193
|
+
return document.querySelector("meta[name='csrf-token']")?.content
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
promptLabel() {
|
|
197
|
+
return this.element.dataset.promptLabel || "Name for lens favorite"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module Favorites
|
|
5
|
+
class Favorite
|
|
6
|
+
class MacroBuilder
|
|
7
|
+
def self.from_template_assignments(assignments)
|
|
8
|
+
assignment_rows(assignments).filter_map do |assignment|
|
|
9
|
+
next unless ActiveModel::Type::Boolean.new.cast(assignment["enabled"])
|
|
10
|
+
|
|
11
|
+
attribute = assignment["attribute"].to_s
|
|
12
|
+
template_name = assignment["name"].to_s
|
|
13
|
+
next if attribute.blank? || template_name.blank?
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
"type" => "template",
|
|
17
|
+
"name" => template_name,
|
|
18
|
+
"attribute" => attribute
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.from_json(json_string)
|
|
24
|
+
parsed = JSON.parse(json_string)
|
|
25
|
+
raise JSON::ParserError unless parsed.is_a?(Array)
|
|
26
|
+
|
|
27
|
+
parsed
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.template(attribute, name:)
|
|
31
|
+
[{"type" => "template", "name" => name.to_s, "attribute" => attribute.to_s}]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.assignment_rows(assignments)
|
|
35
|
+
return [] if assignments.blank?
|
|
36
|
+
|
|
37
|
+
HashCoercion.assignment_values(assignments)
|
|
38
|
+
end
|
|
39
|
+
private_class_method :assignment_rows
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|