ruby_ui_admin 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/MIT-LICENSE +20 -0
- data/README.md +165 -0
- data/app/assets/stylesheets/ruby_ui_admin/application.tailwind.css +111 -0
- data/app/components/ruby_ui_admin/ui/base.rb +26 -0
- data/app/components/ruby_ui_admin/ui/icon.rb +79 -0
- data/app/components/ruby_ui_admin/ui/native_select.rb +37 -0
- data/app/components/ruby_ui_admin/ui/native_select_group.rb +18 -0
- data/app/components/ruby_ui_admin/ui/native_select_icon.rb +42 -0
- data/app/components/ruby_ui_admin/ui/native_select_option.rb +18 -0
- data/app/components/ruby_ui_admin/ui/pagination.rb +62 -0
- data/app/components/ruby_ui_admin/ui/pagination_content.rb +18 -0
- data/app/components/ruby_ui_admin/ui/pagination_ellipsis.rb +45 -0
- data/app/components/ruby_ui_admin/ui/pagination_item.rb +32 -0
- data/app/components/ruby_ui_admin/ui/select.rb +29 -0
- data/app/components/ruby_ui_admin/ui/toast_action.rb +30 -0
- data/app/components/ruby_ui_admin/ui/toast_cancel.rb +30 -0
- data/app/components/ruby_ui_admin/ui/toast_close.rb +43 -0
- data/app/components/ruby_ui_admin/ui/toast_description.rb +21 -0
- data/app/components/ruby_ui_admin/ui/toast_icon.rb +69 -0
- data/app/components/ruby_ui_admin/ui/toast_item.rb +75 -0
- data/app/components/ruby_ui_admin/ui/toast_region.rb +147 -0
- data/app/components/ruby_ui_admin/ui/toast_title.rb +21 -0
- data/app/components/ruby_ui_admin/ui.rb +12 -0
- data/app/components/ruby_ui_admin/views/action.rb +37 -0
- data/app/components/ruby_ui_admin/views/action_form.rb +104 -0
- data/app/components/ruby_ui_admin/views/action_trigger.rb +148 -0
- data/app/components/ruby_ui_admin/views/actions_menu.rb +34 -0
- data/app/components/ruby_ui_admin/views/base.rb +301 -0
- data/app/components/ruby_ui_admin/views/card_component.rb +69 -0
- data/app/components/ruby_ui_admin/views/dashboard.rb +40 -0
- data/app/components/ruby_ui_admin/views/docs.rb +91 -0
- data/app/components/ruby_ui_admin/views/field_input.rb +156 -0
- data/app/components/ruby_ui_admin/views/field_value.rb +301 -0
- data/app/components/ruby_ui_admin/views/filter_bar.rb +111 -0
- data/app/components/ruby_ui_admin/views/form.rb +151 -0
- data/app/components/ruby_ui_admin/views/home.rb +35 -0
- data/app/components/ruby_ui_admin/views/index.rb +324 -0
- data/app/components/ruby_ui_admin/views/path_helpers.rb +36 -0
- data/app/components/ruby_ui_admin/views/rails_helpers.rb +15 -0
- data/app/components/ruby_ui_admin/views/show.rb +77 -0
- data/app/components/ruby_ui_admin/views/show_fields.rb +37 -0
- data/app/components/ruby_ui_admin/views/show_tab.rb +58 -0
- data/app/components/ruby_ui_admin/views/structure_renderer.rb +148 -0
- data/app/components/ruby_ui_admin/views/translation.rb +12 -0
- data/app/controllers/ruby_ui_admin/actions_controller.rb +162 -0
- data/app/controllers/ruby_ui_admin/application_controller.rb +81 -0
- data/app/controllers/ruby_ui_admin/dashboards_controller.rb +12 -0
- data/app/controllers/ruby_ui_admin/docs_controller.rb +166 -0
- data/app/controllers/ruby_ui_admin/home_controller.rb +18 -0
- data/app/controllers/ruby_ui_admin/resources_controller.rb +259 -0
- data/config/locales/ruby_ui_admin.en.yml +59 -0
- data/config/locales/ruby_ui_admin.pt-BR.yml +59 -0
- data/config/routes/dynamic_routes.rb +11 -0
- data/config/routes.rb +23 -0
- data/docs/README.md +53 -0
- data/docs/actions/overview.md +112 -0
- data/docs/authorization/action-policy.md +72 -0
- data/docs/authorization/adapters.md +101 -0
- data/docs/authorization/explicit-mode.md +45 -0
- data/docs/authorization/field-authorization.md +53 -0
- data/docs/authorization/policies.md +62 -0
- data/docs/authorization/scopes.md +48 -0
- data/docs/customization/controllers.md +62 -0
- data/docs/customization/ejecting.md +55 -0
- data/docs/customization/javascript.md +161 -0
- data/docs/customization/theming-rubyui.md +80 -0
- data/docs/dashboards/overview.md +65 -0
- data/docs/fields/overview.md +94 -0
- data/docs/filters/overview.md +73 -0
- data/docs/generators-and-tasks/overview.md +42 -0
- data/docs/getting-started/authentication.md +98 -0
- data/docs/getting-started/configuration.md +174 -0
- data/docs/getting-started/installation.md +201 -0
- data/docs/getting-started/internationalization.md +50 -0
- data/docs/getting-started/practical-guide.md +310 -0
- data/docs/resources/associations.md +66 -0
- data/docs/resources/index-customization.md +68 -0
- data/docs/resources/overview.md +50 -0
- data/docs/resources/scopes.md +54 -0
- data/docs/resources/tabs-panels.md +65 -0
- data/lib/generators/ruby_ui_admin/action/action_generator.rb +21 -0
- data/lib/generators/ruby_ui_admin/action/templates/action.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/assets/assets_generator.rb +71 -0
- data/lib/generators/ruby_ui_admin/card/card_generator.rb +28 -0
- data/lib/generators/ruby_ui_admin/card/templates/chart_card.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/card/templates/metric_card.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/card/templates/partial_card.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/components/components_generator.rb +43 -0
- data/lib/generators/ruby_ui_admin/controller/controller_generator.rb +30 -0
- data/lib/generators/ruby_ui_admin/controller/templates/controller.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/dashboard/dashboard_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/dashboard/templates/dashboard.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/eject/eject_generator.rb +55 -0
- data/lib/generators/ruby_ui_admin/filter/filter_generator.rb +28 -0
- data/lib/generators/ruby_ui_admin/filter/templates/boolean_filter.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/filter/templates/select_filter.rb.tt +19 -0
- data/lib/generators/ruby_ui_admin/filter/templates/text_filter.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/install/install_generator.rb +62 -0
- data/lib/generators/ruby_ui_admin/install/templates/initializer.rb.tt +41 -0
- data/lib/generators/ruby_ui_admin/locales/locales_generator.rb +22 -0
- data/lib/generators/ruby_ui_admin/policy/policy_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/policy/templates/policy.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/resource/resource_generator.rb +84 -0
- data/lib/generators/ruby_ui_admin/resource/templates/resource.rb.tt +15 -0
- data/lib/generators/ruby_ui_admin/scope/scope_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/scope/templates/scope.rb.tt +11 -0
- data/lib/ruby_ui_admin/authorization/action_policy_adapter.rb +69 -0
- data/lib/ruby_ui_admin/authorization/adapter.rb +80 -0
- data/lib/ruby_ui_admin/authorization/can_can_can_adapter.rb +70 -0
- data/lib/ruby_ui_admin/authorization/pundit_adapter.rb +86 -0
- data/lib/ruby_ui_admin/authorization.rb +40 -0
- data/lib/ruby_ui_admin/base_action.rb +211 -0
- data/lib/ruby_ui_admin/base_dashboard.rb +70 -0
- data/lib/ruby_ui_admin/base_policy.rb +38 -0
- data/lib/ruby_ui_admin/base_resource.rb +503 -0
- data/lib/ruby_ui_admin/cards/base_card.rb +59 -0
- data/lib/ruby_ui_admin/cards/chart_card.rb +16 -0
- data/lib/ruby_ui_admin/cards/metric_card.rb +22 -0
- data/lib/ruby_ui_admin/cards/partial_card.rb +14 -0
- data/lib/ruby_ui_admin/configuration.rb +177 -0
- data/lib/ruby_ui_admin/current.rb +20 -0
- data/lib/ruby_ui_admin/dashboard_manager.rb +52 -0
- data/lib/ruby_ui_admin/engine.rb +80 -0
- data/lib/ruby_ui_admin/execution_context.rb +36 -0
- data/lib/ruby_ui_admin/fields/association_field.rb +67 -0
- data/lib/ruby_ui_admin/fields/badge_field.rb +19 -0
- data/lib/ruby_ui_admin/fields/base_field.rb +208 -0
- data/lib/ruby_ui_admin/fields/belongs_to_field.rb +49 -0
- data/lib/ruby_ui_admin/fields/boolean_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/boolean_group_field.rb +54 -0
- data/lib/ruby_ui_admin/fields/code_field.rb +18 -0
- data/lib/ruby_ui_admin/fields/date_field.rb +34 -0
- data/lib/ruby_ui_admin/fields/date_time_field.rb +37 -0
- data/lib/ruby_ui_admin/fields/field_manager.rb +27 -0
- data/lib/ruby_ui_admin/fields/file_field.rb +67 -0
- data/lib/ruby_ui_admin/fields/files_field.rb +62 -0
- data/lib/ruby_ui_admin/fields/has_and_belongs_to_many_field.rb +9 -0
- data/lib/ruby_ui_admin/fields/has_many_field.rb +24 -0
- data/lib/ruby_ui_admin/fields/has_one_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/hidden_field.rb +14 -0
- data/lib/ruby_ui_admin/fields/id_field.rb +19 -0
- data/lib/ruby_ui_admin/fields/key_value_field.rb +41 -0
- data/lib/ruby_ui_admin/fields/number_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/password_field.rb +21 -0
- data/lib/ruby_ui_admin/fields/record_link_field.rb +45 -0
- data/lib/ruby_ui_admin/fields/select_field.rb +71 -0
- data/lib/ruby_ui_admin/fields/status_field.rb +21 -0
- data/lib/ruby_ui_admin/fields/text_field.rb +14 -0
- data/lib/ruby_ui_admin/fields/textarea_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/url_field.rb +21 -0
- data/lib/ruby_ui_admin/filters/base_filter.rb +81 -0
- data/lib/ruby_ui_admin/filters/boolean_filter.rb +9 -0
- data/lib/ruby_ui_admin/filters/multiple_select_filter.rb +11 -0
- data/lib/ruby_ui_admin/filters/select_filter.rb +9 -0
- data/lib/ruby_ui_admin/filters/text_filter.rb +9 -0
- data/lib/ruby_ui_admin/menu/builder.rb +94 -0
- data/lib/ruby_ui_admin/resource_manager.rb +82 -0
- data/lib/ruby_ui_admin/scopes/base_scope.rb +107 -0
- data/lib/ruby_ui_admin/services/authorization_service.rb +38 -0
- data/lib/ruby_ui_admin/structure.rb +40 -0
- data/lib/ruby_ui_admin/tailwind_source.rb +65 -0
- data/lib/ruby_ui_admin/version.rb +5 -0
- data/lib/ruby_ui_admin/view.rb +34 -0
- data/lib/ruby_ui_admin.rb +110 -0
- data/lib/tasks/ruby_ui_admin.rake +71 -0
- data/public/ruby-ui-admin-assets/application.css +2 -0
- data/public/ruby-ui-admin-assets/controllers/index.js +36 -0
- data/public/ruby-ui-admin-assets/controllers/rua--bulk-select_controller.js +19 -0
- data/public/ruby-ui-admin-assets/controllers/rua--confirm_controller.js +51 -0
- data/public/ruby-ui-admin-assets/controllers/rua--dialog_controller.js +55 -0
- data/public/ruby-ui-admin-assets/controllers/rua--row-link_controller.js +16 -0
- data/public/ruby-ui-admin-assets/controllers/rua--tabs_controller.js +36 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--combobox_controller.js +197 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_content_controller.js +7 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_controller.js +15 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sidebar_controller.js +67 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--toast_controller.js +151 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--toaster_controller.js +314 -0
- data/public/ruby-ui-admin-assets/vendor/stimulus.js +2567 -0
- data/public/ruby-ui-admin-assets/vendor/turbo.js +7178 -0
- metadata +379 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Authorization adapters
|
|
2
|
+
|
|
3
|
+
Authorization is **pluggable**. The admin checks every request through an *adapter*, selected with
|
|
4
|
+
`config.authorization_client`:
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
RubyUIAdmin.configure do |config|
|
|
8
|
+
config.authorization_client = :action_policy # default
|
|
9
|
+
# config.authorization_client = :pundit
|
|
10
|
+
# config.authorization_client = :cancancan
|
|
11
|
+
# config.authorization_client = MyApp::CustomAdapter
|
|
12
|
+
# config.authorization_client = nil # disable authorization entirely
|
|
13
|
+
end
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Internally `RubyUIAdmin::Services::AuthorizationService` is a thin facade that resolves the adapter
|
|
17
|
+
(`RubyUIAdmin::Authorization.adapter_class`) and delegates to it, so controllers, views and
|
|
18
|
+
resources stay backend-agnostic.
|
|
19
|
+
|
|
20
|
+
## Built-in adapters
|
|
21
|
+
|
|
22
|
+
| Value | Backend | Requires gem | Policy resolution |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `:action_policy` (default) | [action_policy](https://actionpolicy.evilmartians.io) | — (gem dependency) | per-resource `authorization_policy` (`RubyUIAdmin::BasePolicy` subclass) |
|
|
25
|
+
| `:pundit` | [Pundit](https://github.com/varvet/pundit) | `pundit` | per-resource `authorization_policy`, else Pundit inference |
|
|
26
|
+
| `:cancancan` | [CanCanCan](https://github.com/CanCanCommunity/cancancan) | `cancancan` | a single global `Ability` (`config.cancancan_ability_class`) |
|
|
27
|
+
|
|
28
|
+
`:pundit`/`:cancancan` are loaded **lazily** — the gem only requires them when selected, so they're
|
|
29
|
+
optional dependencies you add to your own `Gemfile`.
|
|
30
|
+
|
|
31
|
+
## Capability matrix
|
|
32
|
+
|
|
33
|
+
| Capability | action_policy | Pundit | CanCanCan |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| CRUD (`index`/`show`/`new`/`edit`/`create`/`update`/`destroy`) | ✅ | ✅ | ✅ |
|
|
36
|
+
| Record scope on the index | ✅ `relation_scope` | ✅ policy `Scope` | ✅ `accessible_by` |
|
|
37
|
+
| Field-level rules (`view_<field>?`, …) | ✅ | ✅ (if the policy defines them) | ❌ not supported (all fields visible) |
|
|
38
|
+
| Custom action auth (`act_on?`) | ✅ | ✅ (if defined) | ⚠️ via the mapped action / `:manage` |
|
|
39
|
+
| `true_user` (impersonation context) | ✅ | ❌ (policies get `user` only) | ❌ (Ability gets `user` only) |
|
|
40
|
+
| `explicit_authorization` fallback (undefined rule → allowed/denied) | ✅ | ✅ | ❌ (CanCanCan denies whatever the Ability doesn't grant) |
|
|
41
|
+
|
|
42
|
+
> **Recommendation:** use `:action_policy` (or `:pundit`) if you need field-level rules,
|
|
43
|
+
> impersonation context, or the `explicit_authorization` fallback. `:cancancan` covers CRUD +
|
|
44
|
+
> record scoping; field-level authorization is silently skipped (every field stays visible).
|
|
45
|
+
|
|
46
|
+
## Using Pundit
|
|
47
|
+
|
|
48
|
+
Add `gem "pundit"`, set `config.authorization_client = :pundit`, and point each resource's
|
|
49
|
+
`authorization_policy` at a Pundit policy (one that responds to `index?`/`show?`/… and, optionally,
|
|
50
|
+
a `Scope`). Rules the policy doesn't define fall back to `explicit_authorization`. Pundit policies
|
|
51
|
+
receive only `user` — `true_user` is not passed.
|
|
52
|
+
|
|
53
|
+
## Using CanCanCan
|
|
54
|
+
|
|
55
|
+
Add `gem "cancancan"`, set `config.authorization_client = :cancancan`, and define a single
|
|
56
|
+
`Ability` (override the class with `config.cancancan_ability_class` if it isn't named `Ability`):
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
class Ability
|
|
60
|
+
include CanCan::Ability
|
|
61
|
+
def initialize(user)
|
|
62
|
+
can :manage, :all if user&.admin?
|
|
63
|
+
can :read, Post, published: true
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The index scope follows the Ability's rules (`accessible_by`). Note the limitations in the matrix:
|
|
69
|
+
no field-level rules, and `explicit_authorization` has no effect (grant access in the Ability).
|
|
70
|
+
|
|
71
|
+
## Writing a custom adapter
|
|
72
|
+
|
|
73
|
+
Subclass `RubyUIAdmin::Authorization::Adapter` and implement the four backend methods. The base
|
|
74
|
+
provides the context (`user`, `true_user`, `record`, `policy_class`), `allowed?`, `normalize_rule`
|
|
75
|
+
and `handle_missing_policy`:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
module MyApp
|
|
79
|
+
class CustomAdapter < RubyUIAdmin::Authorization::Adapter
|
|
80
|
+
# Authorize a rule (`:show`, `:update`, `:act_on`, `:view_<field>`…). Return a boolean; raise
|
|
81
|
+
# RubyUIAdmin::NotAuthorizedError when denied and raise_exception is true.
|
|
82
|
+
def authorize_action(rule, record: nil, raise_exception: true)
|
|
83
|
+
# ... your logic, using `user` / `true_user` / `policy_class` ...
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Apply a record scope to the index relation (return it unchanged when there's no scope).
|
|
87
|
+
def apply_policy(scope) = scope
|
|
88
|
+
|
|
89
|
+
# Does the resolved policy respond to this rule (for the given record)?
|
|
90
|
+
def has_rule?(rule, record: nil) = false
|
|
91
|
+
|
|
92
|
+
# Does the policy CLASS explicitly define this rule? (field-level: undefined ⇒ visible)
|
|
93
|
+
def defines_rule?(rule) = false
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
RubyUIAdmin.configure { |c| c.authorization_client = MyApp::CustomAdapter }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
A custom adapter class is used as-is. To expose it under a symbol instead, add it to
|
|
101
|
+
`RubyUIAdmin::Authorization::REGISTRY`.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Explicit authorization mode
|
|
2
|
+
|
|
3
|
+
`config.explicit_authorization` controls what happens when authorization can't find an
|
|
4
|
+
explicit answer.
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
RubyUIAdmin.configure do |config|
|
|
8
|
+
config.explicit_authorization = false # default
|
|
9
|
+
end
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## What it affects
|
|
13
|
+
|
|
14
|
+
### Resource & action rules
|
|
15
|
+
|
|
16
|
+
For `index?/show?/create?/update?/destroy?/act_on?`, a rule the policy doesn't define falls
|
|
17
|
+
back to the policy's default rule (`manage?`), which `RubyUIAdmin::BasePolicy` ties to this
|
|
18
|
+
flag:
|
|
19
|
+
|
|
20
|
+
| `explicit_authorization` | Undefined rule | No policy resolved at all |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `false` (default) | **allowed** | **allowed** |
|
|
23
|
+
| `true` | **denied** | **denied** |
|
|
24
|
+
|
|
25
|
+
So `true` is a strict, deny-by-default posture: every allowed action must be declared in a
|
|
26
|
+
policy. This matches a security-first setup.
|
|
27
|
+
|
|
28
|
+
### Field rules
|
|
29
|
+
|
|
30
|
+
Field/association visibility (`view_<field>?`) is **not** governed by this flag. A field is
|
|
31
|
+
always visible unless a policy explicitly defines its rule and returns false. This is
|
|
32
|
+
deliberate — turning on `explicit_authorization` must not silently hide every unconfigured
|
|
33
|
+
field. See [Field-level authorization](field-authorization.md).
|
|
34
|
+
|
|
35
|
+
## Disabling authorization
|
|
36
|
+
|
|
37
|
+
Set `config.authorization_client = nil` to turn authorization off entirely (all rules pass,
|
|
38
|
+
scopes are not applied). Useful for a trusted internal tool, or while bootstrapping.
|
|
39
|
+
|
|
40
|
+
## Choosing a mode
|
|
41
|
+
|
|
42
|
+
- **`false`** — convenient: define only the rules that restrict access; everything else is
|
|
43
|
+
open. Good default for small teams / trusted admins.
|
|
44
|
+
- **`true`** — strict: nothing is permitted unless a policy says so. Prefer this when the
|
|
45
|
+
admin is exposed to many roles and you want fail-closed behaviour.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Field-level authorization
|
|
2
|
+
|
|
3
|
+
You can hide individual fields per user by defining rules on the resource's policy. This
|
|
4
|
+
applies everywhere a field would appear: the index columns, the show view, **and** the
|
|
5
|
+
new/edit forms — including param permitting, so an unauthorized field can't be submitted.
|
|
6
|
+
|
|
7
|
+
## Rules
|
|
8
|
+
|
|
9
|
+
For a field `:views_count`, define one of these on the resource policy:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
module RubyUIAdmin
|
|
13
|
+
module Policies
|
|
14
|
+
class PostPolicy < RubyUIAdmin::BasePolicy
|
|
15
|
+
# Generic visibility (all views):
|
|
16
|
+
def view_views_count? = user.admin?
|
|
17
|
+
|
|
18
|
+
# Or per-view (most specific wins):
|
|
19
|
+
def index_views_count? = user.admin? # index column
|
|
20
|
+
def show_views_count? = true # show view
|
|
21
|
+
def edit_views_count? = user.admin? # new + edit forms
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Resolution order
|
|
28
|
+
|
|
29
|
+
For a field `<id>` in a given view, the first **defined** rule wins:
|
|
30
|
+
|
|
31
|
+
| View | Rules checked (in order) |
|
|
32
|
+
|---|---|
|
|
33
|
+
| index | `index_<id>?` → `view_<id>?` |
|
|
34
|
+
| show | `show_<id>?` → `view_<id>?` |
|
|
35
|
+
| new | `new_<id>?` → `edit_<id>?` → `view_<id>?` |
|
|
36
|
+
| edit | `edit_<id>?` → `view_<id>?` |
|
|
37
|
+
|
|
38
|
+
For association fields the `<id>` is the association name (e.g. `view_comments?`).
|
|
39
|
+
|
|
40
|
+
## Default behaviour
|
|
41
|
+
|
|
42
|
+
- A field is **visible** unless a matching rule is defined **and returns false**.
|
|
43
|
+
- An **undefined** rule means "no opinion" — the field stays visible. This is deliberate:
|
|
44
|
+
enabling [`explicit_authorization`](action-policy.md) does **not** silently hide every
|
|
45
|
+
unconfigured field (only resource/action authorization is affected by that flag).
|
|
46
|
+
- Field authorization only runs when the resource has an explicit
|
|
47
|
+
`self.authorization_policy` and `authorization_client` isn't `nil`.
|
|
48
|
+
|
|
49
|
+
## Security
|
|
50
|
+
|
|
51
|
+
Because the same check filters the fields used for **param permitting and assignment**, a
|
|
52
|
+
field hidden from a user is also stripped from `create`/`update` — they can't set it by
|
|
53
|
+
crafting a request.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Policies
|
|
2
|
+
|
|
3
|
+
Authorization is handled by [action_policy](https://actionpolicy.evilmartians.io). Admin
|
|
4
|
+
policies live in `app/ruby_ui_admin/policies` (`RubyUIAdmin::Policies::*`) and inherit
|
|
5
|
+
`RubyUIAdmin::BasePolicy`. Attach one to a resource with `self.authorization_policy`.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# app/ruby_ui_admin/resources/post.rb
|
|
9
|
+
class Post < RubyUIAdmin::BaseResource
|
|
10
|
+
self.authorization_policy = RubyUIAdmin::Policies::PostPolicy
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# app/ruby_ui_admin/policies/post_policy.rb
|
|
14
|
+
module RubyUIAdmin
|
|
15
|
+
module Policies
|
|
16
|
+
class PostPolicy < RubyUIAdmin::BasePolicy
|
|
17
|
+
def index? = true
|
|
18
|
+
def show? = true
|
|
19
|
+
def create? = user.admin?
|
|
20
|
+
def update? = user.admin? || record.user_id == user.id
|
|
21
|
+
def destroy? = user.admin?
|
|
22
|
+
def act_on? = user.admin? # gate custom actions
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Rule mapping
|
|
29
|
+
|
|
30
|
+
| Controller action | Rule | Target |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| index | `index?` | model class |
|
|
33
|
+
| show | `show?` | record |
|
|
34
|
+
| new / create | `create?` | record |
|
|
35
|
+
| edit / update | `update?` | record |
|
|
36
|
+
| destroy | `destroy?` | record |
|
|
37
|
+
| custom actions | `act_on?` | record(s) / model class |
|
|
38
|
+
|
|
39
|
+
Field- and association-level rules (`view_<field>?`, `show_<field>?`, …) are covered in
|
|
40
|
+
[Field-level authorization](field-authorization.md).
|
|
41
|
+
|
|
42
|
+
## Defaults
|
|
43
|
+
|
|
44
|
+
`RubyUIAdmin::BasePolicy` wires action_policy's default rule (`manage?`) to
|
|
45
|
+
[`explicit_authorization`](explicit-mode.md): a rule you don't define falls back to `manage?`,
|
|
46
|
+
which is allowed when `explicit_authorization` is false (the default) and denied when true.
|
|
47
|
+
|
|
48
|
+
`authorize :user` is declared with `allow_nil: true`, so rules must guard a possibly-nil user
|
|
49
|
+
(`user&.admin?`) when the admin can be reached without a signed-in user.
|
|
50
|
+
|
|
51
|
+
## Context
|
|
52
|
+
|
|
53
|
+
The `user` comes from `config.current_user_method`. `true_user` (the real user behind an
|
|
54
|
+
impersonation) is also available, resolved from `config.true_user_method` (defaults to the
|
|
55
|
+
current user) — gate on it with `true_user&.admin?`. To pass *additional* context (e.g. an
|
|
56
|
+
account), declare it in a custom base policy and provide it — see the
|
|
57
|
+
[action_policy docs](https://actionpolicy.evilmartians.io/#/authorization_context).
|
|
58
|
+
|
|
59
|
+
## The current user
|
|
60
|
+
|
|
61
|
+
`RubyUIAdmin::Current.user` exposes the resolved user during a request. In views, use
|
|
62
|
+
`authorized_to?(:rule, record, policy_class:)` to show/hide buttons and links.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Authorization scopes
|
|
2
|
+
|
|
3
|
+
A policy's **scope** filters the index query so each user only sees the records they're
|
|
4
|
+
allowed to — action_policy's equivalent of Pundit's `policy_scope`. Define it with
|
|
5
|
+
`relation_scope` in the resource policy.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
module RubyUIAdmin
|
|
9
|
+
module Policies
|
|
10
|
+
class PostPolicy < RubyUIAdmin::BasePolicy
|
|
11
|
+
relation_scope do |relation|
|
|
12
|
+
next relation if user.admin?
|
|
13
|
+
|
|
14
|
+
relation.where(user_id: user.id) # members see only their own
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## How it's applied
|
|
22
|
+
|
|
23
|
+
On the index, the query flows through:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
model.all → policy relation_scope → named scope → filters → sorting → pagination
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
So the authorization scope runs first and can't be bypassed by filters or named scopes.
|
|
30
|
+
|
|
31
|
+
- If the policy defines **no** `relation_scope`, the relation is returned unchanged (all
|
|
32
|
+
records, subject to the other steps).
|
|
33
|
+
- If `config.authorization_client` is `nil`, scoping is skipped entirely.
|
|
34
|
+
|
|
35
|
+
## Not the same as named scopes
|
|
36
|
+
|
|
37
|
+
This is **record-level authorization**, distinct from [named index scopes](../resources/scopes.md)
|
|
38
|
+
(the tab bar). Both narrow the index query, but:
|
|
39
|
+
|
|
40
|
+
- **Authorization scope** (`relation_scope`) is enforced for security and always applied.
|
|
41
|
+
- **Named scopes** are user-facing tabs the user chooses between; they compose *after* the
|
|
42
|
+
authorization scope.
|
|
43
|
+
|
|
44
|
+
## Using a scope elsewhere
|
|
45
|
+
|
|
46
|
+
Because the index uses the policy scope, a user who isn't allowed to see a record also can't
|
|
47
|
+
reach it through the index. Direct access (show/edit) is still guarded by the per-record
|
|
48
|
+
rules (`show?`, `update?`, …).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Custom controllers & lifecycle hooks
|
|
2
|
+
|
|
3
|
+
Each resource is served by a generic `RubyUIAdmin::ResourcesController`. To customize CRUD
|
|
4
|
+
behaviour for one resource, generate a per-resource controller:
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
rails g ruby_ui_admin:controller Buyer
|
|
8
|
+
# => app/controllers/ruby_ui_admin/buyers_controller.rb
|
|
9
|
+
# class RubyUIAdmin::BuyersController < RubyUIAdmin::ResourcesController
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
When that file exists, the resource's routes point to it automatically (otherwise they use
|
|
13
|
+
the generic controller). Restart the server after adding a new controller so routes redraw.
|
|
14
|
+
|
|
15
|
+
## Lifecycle hooks
|
|
16
|
+
|
|
17
|
+
Override any of these (defaults live in `ResourcesController`):
|
|
18
|
+
|
|
19
|
+
| Hook | Default | When |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `create_success_action` | redirect to `after_create_path`, flash notice | record saved on create |
|
|
22
|
+
| `create_fail_action` | re-render the form (422) | validation failed on create |
|
|
23
|
+
| `update_success_action` | redirect to `after_update_path`, flash notice | record saved on update |
|
|
24
|
+
| `update_fail_action` | re-render the form (422) | validation failed on update |
|
|
25
|
+
| `destroy_success_action` | redirect to `after_destroy_path`, flash notice | record destroyed |
|
|
26
|
+
| `destroy_fail_action` | redirect to the record, flash alert | `destroy` returned false |
|
|
27
|
+
| `after_create_path` / `after_update_path` | the record's show page | used by the success actions |
|
|
28
|
+
| `after_destroy_path` | the resource index | used by destroy success |
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
module RubyUIAdmin
|
|
32
|
+
class BuyersController < RubyUIAdmin::ResourcesController
|
|
33
|
+
# Custom redirect + flash after update
|
|
34
|
+
def update_success_action
|
|
35
|
+
redirect_to resources_index_path, notice: "Buyer updated."
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Delete asynchronously instead of inline
|
|
39
|
+
def destroy_fail_action
|
|
40
|
+
Buyers::DeleteBuyerJob.perform_later(buyer: @record)
|
|
41
|
+
redirect_to resources_index_path, notice: "Deletion started in the background."
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Inside hooks you have the usual controller context: `@record`, `@resource`, `params`,
|
|
48
|
+
`current_user`, `redirect_to`/`render`, `flash`, and the path helpers `resources_index_path`
|
|
49
|
+
and `record_path(record)`.
|
|
50
|
+
|
|
51
|
+
## Overriding the CRUD actions
|
|
52
|
+
|
|
53
|
+
You can also override `create`, `update`, `destroy` (etc.) directly and call `super`:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
def create
|
|
57
|
+
# custom param munging…
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Prefer the [eject generator](ejecting.md) for changing the generic controller or views.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Ejecting & customizing
|
|
2
|
+
|
|
3
|
+
When configuration isn't enough, you can **eject** any engine file into your app and edit it.
|
|
4
|
+
A copy placed at the same path in your app's `app/` shadows the engine's version (Zeitwerk
|
|
5
|
+
loads your app's file first), so the framework picks up your customization automatically.
|
|
6
|
+
|
|
7
|
+
## The generator
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# View components (app/components/ruby_ui_admin/views/*)
|
|
11
|
+
rails g ruby_ui_admin:eject --view index # the index table/page
|
|
12
|
+
rails g ruby_ui_admin:eject --view show
|
|
13
|
+
rails g ruby_ui_admin:eject --view form
|
|
14
|
+
rails g ruby_ui_admin:eject --view layout # alias for the Base layout (sidebar/topbar)
|
|
15
|
+
rails g ruby_ui_admin:eject --view field_value # how field values render
|
|
16
|
+
rails g ruby_ui_admin:eject --view field_input # how form inputs render
|
|
17
|
+
|
|
18
|
+
# Controllers (app/controllers/ruby_ui_admin/*)
|
|
19
|
+
rails g ruby_ui_admin:eject --controller resources # CRUD behaviour
|
|
20
|
+
rails g ruby_ui_admin:eject --controller application # base controller (auth hooks, etc.)
|
|
21
|
+
|
|
22
|
+
# The admin's own UI components (app/components/ruby_ui_admin/ui/*) — to restyle
|
|
23
|
+
rails g ruby_ui_admin:eject --ui icon
|
|
24
|
+
rails g ruby_ui_admin:eject --ui pagination
|
|
25
|
+
rails g ruby_ui_admin:eject --ui select
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The file is copied verbatim to the same relative path in your app. Edit it there.
|
|
29
|
+
|
|
30
|
+
> **Restyling RubyUI primitives** (button, table, badge, card, sidebar, …): those aren't ejected
|
|
31
|
+
> from the gem — the admin renders **your app's** `RubyUI::*` components, so just edit them in
|
|
32
|
+
> `app/components/ruby_ui/*` (where RubyUI generated them). `--ui` only covers the components the
|
|
33
|
+
> gem owns: `icon`, `pagination`, `select`, and the `toast_*` family.
|
|
34
|
+
|
|
35
|
+
## How overriding works
|
|
36
|
+
|
|
37
|
+
The engine's controllers and Phlex components are autoloaded Ruby classes (e.g.
|
|
38
|
+
`RubyUIAdmin::Views::Index`, `RubyUIAdmin::ResourcesController`). Because your app's autoload
|
|
39
|
+
paths take precedence over an engine's, a file at the identical path in your app defines the
|
|
40
|
+
same class and **wins** — no configuration or monkey-patching required.
|
|
41
|
+
|
|
42
|
+
## Common customizations
|
|
43
|
+
|
|
44
|
+
| Goal | Eject |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Change the sidebar/top bar/branding | `--view layout` |
|
|
47
|
+
| Tweak the index table markup | `--view index` |
|
|
48
|
+
| Change how a field type renders | `--view field_value` / `--view field_input` |
|
|
49
|
+
| Override CRUD redirects/messages | `--controller resources` |
|
|
50
|
+
| Add an authentication/authorization hook | `--controller application` |
|
|
51
|
+
| Restyle a RubyUI primitive (button/table/badge/…) | edit your app's `app/components/ruby_ui/*` |
|
|
52
|
+
| Restyle an admin-owned component (icon/pagination/select/toast) | `--ui icon` / `--ui pagination` / … |
|
|
53
|
+
|
|
54
|
+
> Eject only what you need — every ejected file is a copy you now maintain. Prefer
|
|
55
|
+
> [configuration](../getting-started/configuration.md) when it covers your use case.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# JavaScript
|
|
2
|
+
|
|
3
|
+
The admin's interactive behaviour is built with native **Hotwire** — **Stimulus controllers** plus
|
|
4
|
+
**Turbo** (Drive + Frames) — that **progressively enhance** the server-rendered HTML. Everything
|
|
5
|
+
works without JavaScript; Hotwire only improves a few interactions.
|
|
6
|
+
|
|
7
|
+
## How it's delivered
|
|
8
|
+
|
|
9
|
+
The admin renders **your host app's RubyUI components**, so the JavaScript comes from **your app's
|
|
10
|
+
bundler/importmap** — the admin doesn't ship its own runtime. It needs two sets of controllers
|
|
11
|
+
registered:
|
|
12
|
+
|
|
13
|
+
- The admin's own controllers, prefixed **`rua--*`**: `rua--tabs`, `rua--dialog` (action modals),
|
|
14
|
+
`rua--bulk-select`, `rua--row-link`, `rua--confirm` (delete confirmation).
|
|
15
|
+
- **`ruby-ui--toaster` / `ruby-ui--toast`** — the admin renders RubyUI Toast for flash messages
|
|
16
|
+
**even if your app doesn't use RubyUI Toast**, so you probably won't already have these (the
|
|
17
|
+
toaster is patched for server-rendered flash). The other `ruby-ui--*` it uses (`ruby-ui--sidebar`
|
|
18
|
+
/`ruby-ui--sheet`, `ruby-ui--combobox`, …) are the same ones you already use for RubyUI elsewhere
|
|
19
|
+
— combobox/popover depend on `@floating-ui/dom`, so make sure it's available.
|
|
20
|
+
|
|
21
|
+
**Don't wire these by hand.** The gem ships them flat under
|
|
22
|
+
`public/ruby-ui-admin-assets/controllers/` plus an `index.js` that registers each under the right
|
|
23
|
+
identifier. The `ruby_ui_admin:assets` generator copies the whole directory into your app and tells
|
|
24
|
+
you the one import to add:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bin/rails generate ruby_ui_admin:assets
|
|
28
|
+
# Bundler (esbuild/jsbundling/vite): copies to app/javascript/ruby_ui_admin/,
|
|
29
|
+
# then add to application.js: import "./ruby_ui_admin"
|
|
30
|
+
# Importmap + Propshaft: does NOT copy — pins the engine-served (undigested) entrypoint:
|
|
31
|
+
# config/importmap.rb: pin "ruby_ui_admin", to: "/ruby-ui-admin-assets/controllers/index.js"
|
|
32
|
+
# application.js: import "ruby_ui_admin"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Why importmap doesn't copy: Propshaft content-digests copied files, but `index.js`'s relative
|
|
36
|
+
`./x.js` imports resolve to the *undigested* URLs (import maps only remap bare specifiers), which
|
|
37
|
+
Propshaft won't serve → 404. The engine serves the same files **undigested** via Rack::Static, so
|
|
38
|
+
relative imports work there and `@hotwired/stimulus`/`@hotwired/turbo` resolve through your own
|
|
39
|
+
import map.
|
|
40
|
+
|
|
41
|
+
Avoid translating the files into `controllers/rua/…` folders and running
|
|
42
|
+
`stimulus:manifest:update` — the manifest would derive the wrong identifiers. The shipped `index.js`
|
|
43
|
+
already registers everything correctly; you just import it.
|
|
44
|
+
|
|
45
|
+
> Symptom: if flash toasts don't appear after an action, `ruby-ui--toaster` / `ruby-ui--toast`
|
|
46
|
+
> are missing from your bundle — re-run `ruby_ui_admin:assets` and re-add the import.
|
|
47
|
+
|
|
48
|
+
You wire these (plus your stylesheet) into the admin layout through **`config.head_assets`**:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
RubyUIAdmin.configure do |config|
|
|
52
|
+
config.head_assets = lambda do
|
|
53
|
+
safe_join([
|
|
54
|
+
stylesheet_link_tag("application", "data-turbo-track": "reload"),
|
|
55
|
+
javascript_importmap_tags # or javascript_include_tag("application", type: "module")
|
|
56
|
+
])
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`config.head_assets` is a proc evaluated in the Rails view context, so it can call the usual asset
|
|
62
|
+
helpers. When it's `nil`, the layout emits no assets (the page still renders, just unstyled and
|
|
63
|
+
without enhancement). `config.javascript` is a convenience flag your proc can read to skip the JS
|
|
64
|
+
tags.
|
|
65
|
+
|
|
66
|
+
> **Turbo is on.** Navigation uses Turbo Drive (no full-page reloads), so POST→redirect responses
|
|
67
|
+
> use **303 See Other** and the toaster region is re-rendered each visit (it is intentionally not
|
|
68
|
+
> `turbo-permanent`) so flash messages always appear. Lazy tabs and the action modal load through
|
|
69
|
+
> **Turbo Frames**. Caveat: if your *host* app uses Turbo + importmap, the link into the engine must
|
|
70
|
+
> be a full page load (`data-turbo="false"` on that link) so the engine's own importmap applies.
|
|
71
|
+
|
|
72
|
+
## What it enhances
|
|
73
|
+
|
|
74
|
+
### Tabs (`rua--tabs`)
|
|
75
|
+
|
|
76
|
+
Tab groups (`tabs do … end` in a resource) render as stacked, titled cards **without JS**. The
|
|
77
|
+
controller reveals the tab bar, hides the redundant per-panel headings, and shows only the active
|
|
78
|
+
panel. Switching tabs is instant and needs no server round-trip.
|
|
79
|
+
|
|
80
|
+
#### Lazy tabs
|
|
81
|
+
|
|
82
|
+
When `config.lazy_tabs` is on, non-active tabs render only a `<turbo-frame loading="lazy" src="…">`
|
|
83
|
+
(a spinner until it loads). Turbo fetches the fragment URL when the tab becomes visible (the show
|
|
84
|
+
action returns just that tab's content wrapped in the matching `<turbo-frame id>`), and swaps it in
|
|
85
|
+
— no custom fetch; the `rua--tabs` controller only switches panels. Without JS the deferred tabs
|
|
86
|
+
show a `<noscript>` "requires JavaScript" notice. See
|
|
87
|
+
[Tabs & Panels › Lazy tab loading](../resources/tabs-panels.md#lazy-tab-loading).
|
|
88
|
+
|
|
89
|
+
### Action modals (`rua--dialog`)
|
|
90
|
+
|
|
91
|
+
A custom action trigger opens an inline **modal** whose body is a `<turbo-frame>` holding the
|
|
92
|
+
action's form (loaded lazily, so the action's fields aren't evaluated until the modal opens).
|
|
93
|
+
**Non-bulk** modals carry a static `src` + `loading="lazy"` (Turbo loads them when the dialog
|
|
94
|
+
becomes visible); **bulk** modals have no server-side `src` — `rua--dialog#open` sets it from the
|
|
95
|
+
currently checked rows, so reopening with a different selection refetches. The form carries
|
|
96
|
+
`data-turbo-frame="_top"`, so submitting breaks out into a full Turbo visit (POST→303→redirect+flash)
|
|
97
|
+
rather than reloading just the frame. Without JS the same trigger is a plain link to the action's
|
|
98
|
+
page, so actions always work. The modal closes on the ✕ button, the backdrop, the cancel link, or
|
|
99
|
+
the Escape key.
|
|
100
|
+
|
|
101
|
+
### Bulk selection (`rua--bulk-select`)
|
|
102
|
+
|
|
103
|
+
On index tables with bulk actions, the controller reveals the "select all" checkbox (hidden
|
|
104
|
+
without JS) and toggles the per-row record checkboxes; the checked ids are submitted with a bulk
|
|
105
|
+
action. Without JS there is no client-side selection.
|
|
106
|
+
|
|
107
|
+
### Row links (`rua--row-link`)
|
|
108
|
+
|
|
109
|
+
With `config.click_row_to_view_record`, clicking an index row (or a row in a `has_many`
|
|
110
|
+
association table) navigates to the record's show page — ignoring clicks on links/buttons/inputs
|
|
111
|
+
so row controls keep working. Without JS rows aren't clickable (the per-row show/edit links remain).
|
|
112
|
+
|
|
113
|
+
### Delete confirmation (`rua--confirm`)
|
|
114
|
+
|
|
115
|
+
The **Delete** control opens a confirmation **AlertDialog** (shared, on `<body>`) before
|
|
116
|
+
submitting; confirming submits the delete form. Without JS the form submits directly, so deletion
|
|
117
|
+
still works.
|
|
118
|
+
|
|
119
|
+
### Notifications (Toast) — `ruby-ui--toaster` / `ruby-ui--toast`
|
|
120
|
+
|
|
121
|
+
Flash messages (`notice` / `alert`, and any `succeed`/`error`/`inform`/`warn` from an action)
|
|
122
|
+
render through the genuine **RubyUI Toast** component (vendored under `RubyUIAdmin::UI::Toast*`).
|
|
123
|
+
Toasts appear **bottom-right**, with a variant-coloured left border and icon, stack, auto-dismiss,
|
|
124
|
+
and can be swiped or closed. Flash key → variant: `notice`/`success` → **success** (green),
|
|
125
|
+
`alert`/`error` → **error** (red), plus `warning`/`info`. You can also raise them from your own JS:
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
RubyUI.toast.success("Saved")
|
|
129
|
+
RubyUI.toast.error("Something went wrong", { description: "Try again." })
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Without JS, a `<noscript>` fallback shows the messages as badges so they're never lost.
|
|
133
|
+
|
|
134
|
+
### Sidebar — `ruby-ui--sidebar`
|
|
135
|
+
|
|
136
|
+
The left navigation is the genuine **RubyUI Sidebar** (`RubyUIAdmin::UI::Sidebar*`). It is **fixed
|
|
137
|
+
and expanded by default**; the top-bar trigger collapses it to an **icon rail** (`collapsible: :icon`)
|
|
138
|
+
and the choice is **persisted in a cookie** (`sidebar_state`, 7 days). Below `768px` it becomes a
|
|
139
|
+
**drawer** (a RubyUI Sheet). Without JS the sidebar simply renders expanded.
|
|
140
|
+
|
|
141
|
+
## Markup hooks
|
|
142
|
+
|
|
143
|
+
The controllers are driven by standard Stimulus attributes (`data-controller`,
|
|
144
|
+
`data-action`, `data-<id>-target`, `data-<id>-<name>-param/value`) plus a few `data-rua-*` markers
|
|
145
|
+
(handy if you eject/customize views):
|
|
146
|
+
|
|
147
|
+
| Hook | Purpose |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `[data-controller="rua--tabs"]` | tab group; targets `tab`/`panel`/`nav`/`heading`, action `click->rua--tabs#show` |
|
|
150
|
+
| `[data-rua-tab="key"]` / `[data-rua-tab-panel="key"]` | button↔panel key matching |
|
|
151
|
+
| `<turbo-frame id="rua-tab-frame-N" loading="lazy" src="/url">` | a deferred tab panel; Turbo loads it when the tab is shown (`config.lazy_tabs`) |
|
|
152
|
+
| `[data-controller="rua--dialog"]` | manages action modals; trigger `click->rua--dialog#open` + `…-id-param`/`…-bulk-param`, close via `click->rua--dialog#close` |
|
|
153
|
+
| `[data-rua-dialog="id"]` + inner `<turbo-frame>` | the modal; the frame loads the form (`[data-rua-frame-base]` on bulk frames is the base URL the controller appends checked ids to) |
|
|
154
|
+
| `[data-controller="rua--bulk-select"]` | table bulk selection; target `selectAll`, action `change->rua--bulk-select#toggleAll`; `[data-rua-row-select]` marks a record checkbox |
|
|
155
|
+
| `[data-controller="rua--row-link"]` | clickable row; `…-url-value` + `click->rua--row-link#navigate` |
|
|
156
|
+
| `[data-controller="rua--confirm"]` (on `<body>`) | shared confirm AlertDialog; targets `dialog`/`title`/`message`; trigger `click->rua--confirm#request` + `…-message-param`/`…-heading-param`; `#confirm`/`#cancel` |
|
|
157
|
+
| `[data-controller="ruby-ui--toaster"]` / `…--toast` | the RubyUI Toast region/items |
|
|
158
|
+
| `[data-controller="ruby-ui--sidebar"]` + `[data-sidebar="trigger"]` | the RubyUI Sidebar (collapse + mobile drawer) |
|
|
159
|
+
| `[data-controller="ruby-ui--combobox"]` | the RubyUI Combobox (searchable filters and actions menu) |
|
|
160
|
+
|
|
161
|
+
> Progressive enhancement is intentional: the whole admin is usable with JavaScript disabled.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Theming with RubyUI
|
|
2
|
+
|
|
3
|
+
The admin renders **your host app's RubyUI (Phlex) components**, so it inherits your app's RubyUI
|
|
4
|
+
theme automatically — there's no separate admin theme to keep in sync.
|
|
5
|
+
|
|
6
|
+
## How styling is delivered
|
|
7
|
+
|
|
8
|
+
The admin renders the same `RubyUI::*` components you generated into your app, which emit Tailwind
|
|
9
|
+
utility classes. Styling comes from **your app's Tailwind build** — there is no admin-specific
|
|
10
|
+
stylesheet. You must ensure your Tailwind content/sources cover:
|
|
11
|
+
|
|
12
|
+
- `app/components/ruby_ui/**` (your RubyUI components), and
|
|
13
|
+
- the admin's own views (the engine's `app/components/ruby_ui_admin/**`), so the classes used by
|
|
14
|
+
the admin's layout/lists/forms are generated.
|
|
15
|
+
|
|
16
|
+
Then point the admin layout at your compiled CSS via `config.head_assets` (see
|
|
17
|
+
[JavaScript › head assets](javascript.md)). Because the admin uses your RubyUI components, any
|
|
18
|
+
RubyUI design tokens they rely on (e.g. `success` / `warning` colors) must be defined in your
|
|
19
|
+
theme.
|
|
20
|
+
|
|
21
|
+
## Design tokens
|
|
22
|
+
|
|
23
|
+
RubyUI components resolve colors through CSS variables (design tokens) defined in your app's
|
|
24
|
+
RubyUI stylesheet — typically:
|
|
25
|
+
|
|
26
|
+
```css
|
|
27
|
+
:root {
|
|
28
|
+
--background: #ffffff;
|
|
29
|
+
--foreground: #0a0a0a;
|
|
30
|
+
--muted: #f4f4f5; --muted-foreground: #71717a;
|
|
31
|
+
--border: #e4e4e7; --input: #e4e4e7; --ring: #18181b;
|
|
32
|
+
--primary: #18181b; --primary-foreground: #fafafa;
|
|
33
|
+
--secondary: #f4f4f5; --secondary-foreground: #18181b;
|
|
34
|
+
--accent: #f4f4f5; --accent-foreground: #18181b;
|
|
35
|
+
--destructive: #ef4444;
|
|
36
|
+
--popover: #ffffff; --popover-foreground: #0a0a0a;
|
|
37
|
+
/* Sidebar (left navigation) */
|
|
38
|
+
--sidebar: #fafafa; --sidebar-foreground: #3f3f46;
|
|
39
|
+
--sidebar-accent: #f4f4f5; --sidebar-accent-foreground: #18181b;
|
|
40
|
+
--sidebar-border: #e5e7eb; --sidebar-ring: #3b82f6;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Utilities like `bg-background`, `text-muted-foreground`, `border-border`, `bg-primary/90`,
|
|
45
|
+
`bg-sidebar`, `text-sidebar-accent-foreground` resolve through these variables, so re-theming
|
|
46
|
+
is a matter of changing the token values. The `--sidebar-*` tokens style the left navigation
|
|
47
|
+
independently from the page (so you can give it its own surface colour).
|
|
48
|
+
|
|
49
|
+
## Dark mode
|
|
50
|
+
|
|
51
|
+
A `.dark` variant is included. Add the `dark` class to a parent element (e.g. `<html>`) to
|
|
52
|
+
switch. The tokens above have dark overrides bundled.
|
|
53
|
+
|
|
54
|
+
## Make sure your Tailwind build covers the admin
|
|
55
|
+
|
|
56
|
+
Because the admin renders with your app's RubyUI components and its own Phlex views, your
|
|
57
|
+
Tailwind build's content/sources must include both, or admin pages will be missing styles:
|
|
58
|
+
|
|
59
|
+
- `app/components/ruby_ui/**` — your RubyUI components, and
|
|
60
|
+
- the admin's views — the engine's `app/components/ruby_ui_admin/**` (in the installed gem).
|
|
61
|
+
|
|
62
|
+
Rather than hardcode the gem's path in `@source` (it varies per environment), run
|
|
63
|
+
`bin/rails ruby_ui_admin:tailwind_source` to extract the admin's class inventory into
|
|
64
|
+
`app/assets/tailwind/ruby_ui_admin_classes.html`, `@source` that (commit it; re-run on upgrade), then
|
|
65
|
+
expose the compiled stylesheet to the admin layout through `config.head_assets`.
|
|
66
|
+
|
|
67
|
+
## Integrating with the host's own Tailwind build (advanced)
|
|
68
|
+
|
|
69
|
+
If you'd rather own the build (e.g. to override tokens app-wide or share your design system),
|
|
70
|
+
point your host Tailwind v4 entry at the engine's components and define the tokens yourself:
|
|
71
|
+
|
|
72
|
+
```css
|
|
73
|
+
@import "tailwindcss";
|
|
74
|
+
@source "./ruby_ui_admin_classes.html"; /* generated by rake ruby_ui_admin:tailwind_source */
|
|
75
|
+
/* define the :root tokens above (or reuse your existing RubyUI tokens) */
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then disable the bundled stylesheet by ejecting the layout, or simply let both load (the host
|
|
79
|
+
build wins by load order). Apps that already use RubyUI (with the same token vocabulary) only
|
|
80
|
+
need the `@source` line.
|