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.
Files changed (182) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +165 -0
  4. data/app/assets/stylesheets/ruby_ui_admin/application.tailwind.css +111 -0
  5. data/app/components/ruby_ui_admin/ui/base.rb +26 -0
  6. data/app/components/ruby_ui_admin/ui/icon.rb +79 -0
  7. data/app/components/ruby_ui_admin/ui/native_select.rb +37 -0
  8. data/app/components/ruby_ui_admin/ui/native_select_group.rb +18 -0
  9. data/app/components/ruby_ui_admin/ui/native_select_icon.rb +42 -0
  10. data/app/components/ruby_ui_admin/ui/native_select_option.rb +18 -0
  11. data/app/components/ruby_ui_admin/ui/pagination.rb +62 -0
  12. data/app/components/ruby_ui_admin/ui/pagination_content.rb +18 -0
  13. data/app/components/ruby_ui_admin/ui/pagination_ellipsis.rb +45 -0
  14. data/app/components/ruby_ui_admin/ui/pagination_item.rb +32 -0
  15. data/app/components/ruby_ui_admin/ui/select.rb +29 -0
  16. data/app/components/ruby_ui_admin/ui/toast_action.rb +30 -0
  17. data/app/components/ruby_ui_admin/ui/toast_cancel.rb +30 -0
  18. data/app/components/ruby_ui_admin/ui/toast_close.rb +43 -0
  19. data/app/components/ruby_ui_admin/ui/toast_description.rb +21 -0
  20. data/app/components/ruby_ui_admin/ui/toast_icon.rb +69 -0
  21. data/app/components/ruby_ui_admin/ui/toast_item.rb +75 -0
  22. data/app/components/ruby_ui_admin/ui/toast_region.rb +147 -0
  23. data/app/components/ruby_ui_admin/ui/toast_title.rb +21 -0
  24. data/app/components/ruby_ui_admin/ui.rb +12 -0
  25. data/app/components/ruby_ui_admin/views/action.rb +37 -0
  26. data/app/components/ruby_ui_admin/views/action_form.rb +104 -0
  27. data/app/components/ruby_ui_admin/views/action_trigger.rb +148 -0
  28. data/app/components/ruby_ui_admin/views/actions_menu.rb +34 -0
  29. data/app/components/ruby_ui_admin/views/base.rb +301 -0
  30. data/app/components/ruby_ui_admin/views/card_component.rb +69 -0
  31. data/app/components/ruby_ui_admin/views/dashboard.rb +40 -0
  32. data/app/components/ruby_ui_admin/views/docs.rb +91 -0
  33. data/app/components/ruby_ui_admin/views/field_input.rb +156 -0
  34. data/app/components/ruby_ui_admin/views/field_value.rb +301 -0
  35. data/app/components/ruby_ui_admin/views/filter_bar.rb +111 -0
  36. data/app/components/ruby_ui_admin/views/form.rb +151 -0
  37. data/app/components/ruby_ui_admin/views/home.rb +35 -0
  38. data/app/components/ruby_ui_admin/views/index.rb +324 -0
  39. data/app/components/ruby_ui_admin/views/path_helpers.rb +36 -0
  40. data/app/components/ruby_ui_admin/views/rails_helpers.rb +15 -0
  41. data/app/components/ruby_ui_admin/views/show.rb +77 -0
  42. data/app/components/ruby_ui_admin/views/show_fields.rb +37 -0
  43. data/app/components/ruby_ui_admin/views/show_tab.rb +58 -0
  44. data/app/components/ruby_ui_admin/views/structure_renderer.rb +148 -0
  45. data/app/components/ruby_ui_admin/views/translation.rb +12 -0
  46. data/app/controllers/ruby_ui_admin/actions_controller.rb +162 -0
  47. data/app/controllers/ruby_ui_admin/application_controller.rb +81 -0
  48. data/app/controllers/ruby_ui_admin/dashboards_controller.rb +12 -0
  49. data/app/controllers/ruby_ui_admin/docs_controller.rb +166 -0
  50. data/app/controllers/ruby_ui_admin/home_controller.rb +18 -0
  51. data/app/controllers/ruby_ui_admin/resources_controller.rb +259 -0
  52. data/config/locales/ruby_ui_admin.en.yml +59 -0
  53. data/config/locales/ruby_ui_admin.pt-BR.yml +59 -0
  54. data/config/routes/dynamic_routes.rb +11 -0
  55. data/config/routes.rb +23 -0
  56. data/docs/README.md +53 -0
  57. data/docs/actions/overview.md +112 -0
  58. data/docs/authorization/action-policy.md +72 -0
  59. data/docs/authorization/adapters.md +101 -0
  60. data/docs/authorization/explicit-mode.md +45 -0
  61. data/docs/authorization/field-authorization.md +53 -0
  62. data/docs/authorization/policies.md +62 -0
  63. data/docs/authorization/scopes.md +48 -0
  64. data/docs/customization/controllers.md +62 -0
  65. data/docs/customization/ejecting.md +55 -0
  66. data/docs/customization/javascript.md +161 -0
  67. data/docs/customization/theming-rubyui.md +80 -0
  68. data/docs/dashboards/overview.md +65 -0
  69. data/docs/fields/overview.md +94 -0
  70. data/docs/filters/overview.md +73 -0
  71. data/docs/generators-and-tasks/overview.md +42 -0
  72. data/docs/getting-started/authentication.md +98 -0
  73. data/docs/getting-started/configuration.md +174 -0
  74. data/docs/getting-started/installation.md +201 -0
  75. data/docs/getting-started/internationalization.md +50 -0
  76. data/docs/getting-started/practical-guide.md +310 -0
  77. data/docs/resources/associations.md +66 -0
  78. data/docs/resources/index-customization.md +68 -0
  79. data/docs/resources/overview.md +50 -0
  80. data/docs/resources/scopes.md +54 -0
  81. data/docs/resources/tabs-panels.md +65 -0
  82. data/lib/generators/ruby_ui_admin/action/action_generator.rb +21 -0
  83. data/lib/generators/ruby_ui_admin/action/templates/action.rb.tt +24 -0
  84. data/lib/generators/ruby_ui_admin/assets/assets_generator.rb +71 -0
  85. data/lib/generators/ruby_ui_admin/card/card_generator.rb +28 -0
  86. data/lib/generators/ruby_ui_admin/card/templates/chart_card.rb.tt +14 -0
  87. data/lib/generators/ruby_ui_admin/card/templates/metric_card.rb.tt +14 -0
  88. data/lib/generators/ruby_ui_admin/card/templates/partial_card.rb.tt +13 -0
  89. data/lib/generators/ruby_ui_admin/components/components_generator.rb +43 -0
  90. data/lib/generators/ruby_ui_admin/controller/controller_generator.rb +30 -0
  91. data/lib/generators/ruby_ui_admin/controller/templates/controller.rb.tt +24 -0
  92. data/lib/generators/ruby_ui_admin/dashboard/dashboard_generator.rb +18 -0
  93. data/lib/generators/ruby_ui_admin/dashboard/templates/dashboard.rb.tt +14 -0
  94. data/lib/generators/ruby_ui_admin/eject/eject_generator.rb +55 -0
  95. data/lib/generators/ruby_ui_admin/filter/filter_generator.rb +28 -0
  96. data/lib/generators/ruby_ui_admin/filter/templates/boolean_filter.rb.tt +13 -0
  97. data/lib/generators/ruby_ui_admin/filter/templates/select_filter.rb.tt +19 -0
  98. data/lib/generators/ruby_ui_admin/filter/templates/text_filter.rb.tt +13 -0
  99. data/lib/generators/ruby_ui_admin/install/install_generator.rb +62 -0
  100. data/lib/generators/ruby_ui_admin/install/templates/initializer.rb.tt +41 -0
  101. data/lib/generators/ruby_ui_admin/locales/locales_generator.rb +22 -0
  102. data/lib/generators/ruby_ui_admin/policy/policy_generator.rb +18 -0
  103. data/lib/generators/ruby_ui_admin/policy/templates/policy.rb.tt +24 -0
  104. data/lib/generators/ruby_ui_admin/resource/resource_generator.rb +84 -0
  105. data/lib/generators/ruby_ui_admin/resource/templates/resource.rb.tt +15 -0
  106. data/lib/generators/ruby_ui_admin/scope/scope_generator.rb +18 -0
  107. data/lib/generators/ruby_ui_admin/scope/templates/scope.rb.tt +11 -0
  108. data/lib/ruby_ui_admin/authorization/action_policy_adapter.rb +69 -0
  109. data/lib/ruby_ui_admin/authorization/adapter.rb +80 -0
  110. data/lib/ruby_ui_admin/authorization/can_can_can_adapter.rb +70 -0
  111. data/lib/ruby_ui_admin/authorization/pundit_adapter.rb +86 -0
  112. data/lib/ruby_ui_admin/authorization.rb +40 -0
  113. data/lib/ruby_ui_admin/base_action.rb +211 -0
  114. data/lib/ruby_ui_admin/base_dashboard.rb +70 -0
  115. data/lib/ruby_ui_admin/base_policy.rb +38 -0
  116. data/lib/ruby_ui_admin/base_resource.rb +503 -0
  117. data/lib/ruby_ui_admin/cards/base_card.rb +59 -0
  118. data/lib/ruby_ui_admin/cards/chart_card.rb +16 -0
  119. data/lib/ruby_ui_admin/cards/metric_card.rb +22 -0
  120. data/lib/ruby_ui_admin/cards/partial_card.rb +14 -0
  121. data/lib/ruby_ui_admin/configuration.rb +177 -0
  122. data/lib/ruby_ui_admin/current.rb +20 -0
  123. data/lib/ruby_ui_admin/dashboard_manager.rb +52 -0
  124. data/lib/ruby_ui_admin/engine.rb +80 -0
  125. data/lib/ruby_ui_admin/execution_context.rb +36 -0
  126. data/lib/ruby_ui_admin/fields/association_field.rb +67 -0
  127. data/lib/ruby_ui_admin/fields/badge_field.rb +19 -0
  128. data/lib/ruby_ui_admin/fields/base_field.rb +208 -0
  129. data/lib/ruby_ui_admin/fields/belongs_to_field.rb +49 -0
  130. data/lib/ruby_ui_admin/fields/boolean_field.rb +13 -0
  131. data/lib/ruby_ui_admin/fields/boolean_group_field.rb +54 -0
  132. data/lib/ruby_ui_admin/fields/code_field.rb +18 -0
  133. data/lib/ruby_ui_admin/fields/date_field.rb +34 -0
  134. data/lib/ruby_ui_admin/fields/date_time_field.rb +37 -0
  135. data/lib/ruby_ui_admin/fields/field_manager.rb +27 -0
  136. data/lib/ruby_ui_admin/fields/file_field.rb +67 -0
  137. data/lib/ruby_ui_admin/fields/files_field.rb +62 -0
  138. data/lib/ruby_ui_admin/fields/has_and_belongs_to_many_field.rb +9 -0
  139. data/lib/ruby_ui_admin/fields/has_many_field.rb +24 -0
  140. data/lib/ruby_ui_admin/fields/has_one_field.rb +13 -0
  141. data/lib/ruby_ui_admin/fields/hidden_field.rb +14 -0
  142. data/lib/ruby_ui_admin/fields/id_field.rb +19 -0
  143. data/lib/ruby_ui_admin/fields/key_value_field.rb +41 -0
  144. data/lib/ruby_ui_admin/fields/number_field.rb +13 -0
  145. data/lib/ruby_ui_admin/fields/password_field.rb +21 -0
  146. data/lib/ruby_ui_admin/fields/record_link_field.rb +45 -0
  147. data/lib/ruby_ui_admin/fields/select_field.rb +71 -0
  148. data/lib/ruby_ui_admin/fields/status_field.rb +21 -0
  149. data/lib/ruby_ui_admin/fields/text_field.rb +14 -0
  150. data/lib/ruby_ui_admin/fields/textarea_field.rb +13 -0
  151. data/lib/ruby_ui_admin/fields/url_field.rb +21 -0
  152. data/lib/ruby_ui_admin/filters/base_filter.rb +81 -0
  153. data/lib/ruby_ui_admin/filters/boolean_filter.rb +9 -0
  154. data/lib/ruby_ui_admin/filters/multiple_select_filter.rb +11 -0
  155. data/lib/ruby_ui_admin/filters/select_filter.rb +9 -0
  156. data/lib/ruby_ui_admin/filters/text_filter.rb +9 -0
  157. data/lib/ruby_ui_admin/menu/builder.rb +94 -0
  158. data/lib/ruby_ui_admin/resource_manager.rb +82 -0
  159. data/lib/ruby_ui_admin/scopes/base_scope.rb +107 -0
  160. data/lib/ruby_ui_admin/services/authorization_service.rb +38 -0
  161. data/lib/ruby_ui_admin/structure.rb +40 -0
  162. data/lib/ruby_ui_admin/tailwind_source.rb +65 -0
  163. data/lib/ruby_ui_admin/version.rb +5 -0
  164. data/lib/ruby_ui_admin/view.rb +34 -0
  165. data/lib/ruby_ui_admin.rb +110 -0
  166. data/lib/tasks/ruby_ui_admin.rake +71 -0
  167. data/public/ruby-ui-admin-assets/application.css +2 -0
  168. data/public/ruby-ui-admin-assets/controllers/index.js +36 -0
  169. data/public/ruby-ui-admin-assets/controllers/rua--bulk-select_controller.js +19 -0
  170. data/public/ruby-ui-admin-assets/controllers/rua--confirm_controller.js +51 -0
  171. data/public/ruby-ui-admin-assets/controllers/rua--dialog_controller.js +55 -0
  172. data/public/ruby-ui-admin-assets/controllers/rua--row-link_controller.js +16 -0
  173. data/public/ruby-ui-admin-assets/controllers/rua--tabs_controller.js +36 -0
  174. data/public/ruby-ui-admin-assets/controllers/ruby_ui--combobox_controller.js +197 -0
  175. data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_content_controller.js +7 -0
  176. data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_controller.js +15 -0
  177. data/public/ruby-ui-admin-assets/controllers/ruby_ui--sidebar_controller.js +67 -0
  178. data/public/ruby-ui-admin-assets/controllers/ruby_ui--toast_controller.js +151 -0
  179. data/public/ruby-ui-admin-assets/controllers/ruby_ui--toaster_controller.js +314 -0
  180. data/public/ruby-ui-admin-assets/vendor/stimulus.js +2567 -0
  181. data/public/ruby-ui-admin-assets/vendor/turbo.js +7178 -0
  182. metadata +379 -0
@@ -0,0 +1,201 @@
1
+ # Installation
2
+
3
+ > **The admin renders *your app's* RubyUI components and uses *your app's* assets.** So setup is
4
+ > mostly about pointing the admin at your existing RubyUI + Tailwind + JS bundler. If your app
5
+ > already uses RubyUI, most of this is already in place.
6
+
7
+ ## 1. Add the gems
8
+
9
+ ```ruby
10
+ # Gemfile
11
+ gem "ruby_ui_admin"
12
+
13
+ # Prerequisite — the UI is built from your app's RubyUI components.
14
+ gem "ruby_ui", github: "ruby-ui/ruby_ui", require: false
15
+ ```
16
+
17
+ ```bash
18
+ bundle install
19
+ ```
20
+
21
+ `ruby_ui_admin` depends on `phlex-rails`, `tailwind_merge`, `action_policy`, `pagy` and
22
+ `turbo-rails` (Bundler installs them automatically). It also expects a **Tailwind build** and a
23
+ **JavaScript bundler or importmap** in your app — the same setup RubyUI itself needs.
24
+
25
+ ## 2. Install RubyUI and generate its components
26
+
27
+ If your app doesn't already use RubyUI, install it and generate the components the admin uses —
28
+ at minimum: `card`, `badge`, `button`, `link`, `typography` (provides `InlineLink`), `checkbox`,
29
+ `input`, `textarea`, `table`, `sidebar`, `sheet`, `combobox`.
30
+
31
+ > **Run `phlex:install` first.** RubyUI's installer only runs `phlex:install` (which creates
32
+ > `app/components/base.rb`) when `phlex-rails` isn't already in the bundle. Because `ruby_ui_admin`
33
+ > depends on `phlex-rails`, it skips that step and then aborts when `app/components/base.rb` is
34
+ > missing — leaving `RubyUI::Base` ungenerated (and `tw-animate-css` uninstalled), which breaks boot
35
+ > when you generate components. Running `phlex:install` yourself first avoids the whole chain.
36
+
37
+ ```bash
38
+ bin/rails generate phlex:install # creates app/components/base.rb (see note above)
39
+ bin/rails generate ruby_ui:install
40
+ ```
41
+
42
+ > `ruby_ui:install` **overwrites** your Tailwind entry CSS behind an interactive prompt — run it on
43
+ > its own (not pasted inside a larger block) and answer `Y`, then re-add your customizations (e.g. the
44
+ > `@source` line below). On **Tailwind v4** (tailwindcss-rails 3+, the current default) that file is
45
+ > `app/assets/tailwind/application.css`; older setups use
46
+ > `app/assets/stylesheets/application.tailwind.css`. It also installs `tw-animate-css`;
47
+ > if a later CSS build errors on it, add it manually (`yarn add tw-animate-css` / importmap pin) and
48
+ > ensure your CSS imports it.
49
+
50
+ Then generate the components the admin renders. The gem knows which ones it needs, so let it drive
51
+ RubyUI's generator for you (safe to re-run — existing components are skipped):
52
+
53
+ ```bash
54
+ bin/rails generate ruby_ui_admin:components
55
+ ```
56
+
57
+ <details><summary>What it runs (and why not one command)</summary>
58
+
59
+ RubyUI's generator takes **one component per command**, and **`InlineLink` is part of the
60
+ `Typography` group** (a bare `ruby_ui:component InlineLink` fails silently and the admin's index
61
+ later breaks, since it renders `RubyUI::InlineLink`). The generator loops over exactly the set the
62
+ admin uses:
63
+
64
+ ```bash
65
+ for c in Card Badge Button Checkbox Combobox Typography Link Input Textarea Table Sidebar Sheet; do
66
+ bin/rails generate ruby_ui:component $c --skip
67
+ done
68
+ # (or `bin/rails generate ruby_ui:component all` to generate everything)
69
+ ```
70
+ </details>
71
+
72
+ This drops the components into `app/components/ruby_ui/*` (as `RubyUI::*`) and copies RubyUI's
73
+ Stimulus controllers into `app/javascript/controllers/ruby_ui/*`. **You own these files.**
74
+
75
+ ## 3. Wire the assets
76
+
77
+ The admin doesn't ship CSS/JS — it reuses yours.
78
+
79
+ **a. Tailwind** — your build must generate the utility classes the admin's views use. Rather than
80
+ `@source`-ing the gem's `.rb` files (their absolute path differs between dev/CI/deploy, so it can't
81
+ be committed), extract the class inventory into your **own** app tree and source that:
82
+
83
+ ```bash
84
+ bin/rails ruby_ui_admin:tailwind_source
85
+ ```
86
+
87
+ It writes `app/assets/tailwind/ruby_ui_admin_classes.html` (the classes scanned from the admin's
88
+ views) and prints the line to add to your Tailwind entry CSS:
89
+
90
+ ```css
91
+ /* Tailwind v4: app/assets/tailwind/application.css (v3: app/assets/stylesheets/application.tailwind.css) */
92
+ @source "./ruby_ui_admin_classes.html";
93
+ ```
94
+
95
+ **Commit the generated file** — that stable, relative path works in every environment. **Re-run the
96
+ task after upgrading the gem** so new classes are picked up. Also make sure your RubyUI theme defines
97
+ the `success` / `warning` design tokens (the admin's badges/toasts use them — RubyUI ships them).
98
+
99
+ **b. Stimulus controllers** — the admin needs two sets registered in your Stimulus app:
100
+
101
+ 1. **The admin's own `rua--*` controllers** (`tabs`, `dialog`, `bulk-select`, `row-link`,
102
+ `confirm`).
103
+ 2. **`ruby-ui--toaster` + `ruby-ui--toast`** — the admin renders RubyUI's **Toast** for flash
104
+ messages **even if your app doesn't use RubyUI Toast**, so you likely won't have these from
105
+ step 2. (The other `ruby-ui--*` it uses — combobox, sheet, sidebar — *do* come from your own
106
+ RubyUI in step 2.)
107
+
108
+ The gem ships both sets, flat, under `public/ruby-ui-admin-assets/controllers/` (the toaster
109
+ controller is patched for server-rendered flash toasts — use the gem's copy), alongside an
110
+ `index.js` that imports and **registers each one under the identifier its markup expects**. Run the
111
+ generator — it does the right thing for your JS setup and prints the **one line** you add:
112
+
113
+ ```bash
114
+ bin/rails generate ruby_ui_admin:assets
115
+ ```
116
+
117
+ **Bundler (esbuild / jsbundling / vite).** It copies the directory to `app/javascript/ruby_ui_admin/`
118
+ (a sibling of `controllers/`, so `eagerLoadControllersFrom("controllers", …)` doesn't re-register
119
+ them under bogus names). The bundler inlines `index.js`'s relative imports, so:
120
+
121
+ ```js
122
+ // app/javascript/application.js
123
+ import "./ruby_ui_admin"
124
+ ```
125
+
126
+ **Importmap + Propshaft.** It does **not** copy — copied files get content-digested, and `index.js`'s
127
+ relative `./x.js` imports would then resolve to undigested URLs Propshaft won't serve (404). Instead
128
+ it pins the copy the engine already serves **undigested** (via Rack::Static), where relative imports
129
+ work and `@hotwired/stimulus`/`@hotwired/turbo` resolve through your own import map:
130
+
131
+ ```ruby
132
+ # config/importmap.rb
133
+ pin "ruby_ui_admin", to: "/ruby-ui-admin-assets/controllers/index.js"
134
+ # then in app/javascript/application.js: import "ruby_ui_admin"
135
+ ```
136
+
137
+ Either way that single import registers every controller (`rua--*`, `ruby-ui--toaster`/`ruby-ui--toast`,
138
+ plus combobox/sheet/sidebar) — **no `stimulus:manifest:update`**. Rebuild your JS (bundler) afterwards.
139
+ Don't hand-translate the files into `controllers/rua/…` folders — the manifest would derive the wrong
140
+ identifiers.
141
+
142
+ > If flash toasts don't appear after an action, it's almost always the `ruby-ui--toaster` /
143
+ > `ruby-ui--toast` controllers missing from your bundle — re-run the generator and re-add the import.
144
+
145
+ RubyUI's combobox/popover controllers depend on `@floating-ui/dom` — already present if you use
146
+ RubyUI's combobox.
147
+
148
+ ## 4. Mount the engine
149
+
150
+ ```ruby
151
+ # config/routes.rb
152
+ Rails.application.routes.draw do
153
+ mount_ruby_ui_admin at: "/admin"
154
+ end
155
+ ```
156
+
157
+ Draws a RESTful route set for every resource you define (`resources_posts_path`,
158
+ `new_resources_post_path`, `edit_resources_post_path`, …).
159
+
160
+ ## 5. Configure
161
+
162
+ ```ruby
163
+ # config/initializers/ruby_ui_admin.rb
164
+ RubyUIAdmin.configure do |config|
165
+ config.app_name = "My Admin"
166
+ config.per_page = 24
167
+
168
+ # Resolve the signed-in admin (the block runs in the controller context).
169
+ config.current_user_method { current_user }
170
+
171
+ # Optional gate run before every admin request.
172
+ config.authenticate_with { redirect_to main_app.login_path unless current_user&.admin? }
173
+
174
+ config.authorization_client = :action_policy
175
+ config.explicit_authorization = false
176
+
177
+ # REQUIRED — the admin layout's <head>. Point it at your app's compiled CSS + JS (the same
178
+ # tags your application layout uses). Evaluated in the Rails view context.
179
+ config.head_assets = lambda do
180
+ safe_join([
181
+ stylesheet_link_tag("application", "data-turbo-track": "reload"),
182
+ # importmap host: use `javascript_importmap_tags` instead of the line below.
183
+ javascript_include_tag("application", "data-turbo-track": "reload", defer: true)
184
+ ])
185
+ end
186
+ end
187
+ ```
188
+
189
+ > Without `config.head_assets`, the admin pages render with **no styling and no JavaScript**.
190
+
191
+ ## 6. Define a resource
192
+
193
+ Create `app/ruby_ui_admin/resources/post.rb` (see [Resources → Overview](../resources/overview.md)).
194
+
195
+ ---
196
+
197
+ `bin/rails generate ruby_ui_admin:install` scaffolds steps **4–5** (the initializer + mount), and
198
+ `ruby_ui_admin:assets` copies the Stimulus controllers from step **3**. The rest of steps **2–3**
199
+ (RubyUI components + Tailwind `@source`) depend on your app's bundler/Tailwind setup and are
200
+ manual — see [Theming with RubyUI](../customization/theming-rubyui.md) and
201
+ [JavaScript](../customization/javascript.md).
@@ -0,0 +1,50 @@
1
+ # Internationalization (i18n)
2
+
3
+ The admin is localized through Rails i18n. Each admin request runs inside
4
+ `I18n.with_locale(config.locale)` when a locale is configured.
5
+
6
+ ```ruby
7
+ RubyUIAdmin.configure do |config|
8
+ config.locale = :"pt-BR"
9
+ # or resolve per request from your controller, e.g. config.locale = I18n.locale
10
+ end
11
+ ```
12
+
13
+ There are two layers of translation:
14
+
15
+ ## 1. Framework strings (bundled)
16
+
17
+ Built-in UI text — buttons, table headers, the empty state, pagination, boolean badges — is
18
+ translated under the `ruby_ui_admin.*` namespace. The gem ships **English** and
19
+ **Brazilian Portuguese** out of the box, auto-loaded into `I18n`.
20
+
21
+ Copy them into your app to edit or add locales:
22
+
23
+ ```bash
24
+ rails g ruby_ui_admin:locales # -> config/locales/ruby_ui_admin.en.yml, ruby_ui_admin.pt-BR.yml
25
+ ```
26
+
27
+ Add another language by creating `config/locales/ruby_ui_admin.<locale>.yml` with the same keys.
28
+
29
+ ## 2. Domain labels (your app's translations)
30
+
31
+ Resource and field labels use standard Rails i18n, so they read from **your** locale files:
32
+
33
+ - **Field labels** use `Model.human_attribute_name`, i.e. `activerecord.attributes.<model>.<attr>`.
34
+ - They fall back to a humanized name when there's no translation.
35
+
36
+ ```yaml
37
+ # config/locales/pt-BR.yml
38
+ "pt-BR":
39
+ activerecord:
40
+ attributes:
41
+ post:
42
+ title: "Título"
43
+ published: "Publicado"
44
+ ```
45
+
46
+ With the locale set to `pt-BR`, the Post resource's `title` field renders as “Título”.
47
+
48
+ > Action names are plain strings you set with `self.name`; localize them by setting the name
49
+ > from a translation in your action (e.g. `self.name = I18n.t("...")` is evaluated at load —
50
+ > prefer a fixed string or your own lookup if you need per-request locales).
@@ -0,0 +1,310 @@
1
+ # Practical guide: a working admin from scratch
2
+
3
+ This is a copy‑paste walkthrough that takes a brand‑new Rails app to a functional admin with
4
+ two associated models, configured resources, and a couple of filters. It uses a small
5
+ **Bookshelf** domain: an `Author` that `has_many :books`.
6
+
7
+ The demo skips authentication and authorization so it "just works" — see the
8
+ [Authentication](authentication.md) and [Authorization](../authorization/action-policy.md)
9
+ docs to wire those up for real.
10
+
11
+ ---
12
+
13
+ ## 1. Create the Rails app
14
+
15
+ The admin renders RubyUI (Phlex) components styled with Tailwind, so start the app with a
16
+ Tailwind build and a JS bundler:
17
+
18
+ ```bash
19
+ rails new bookshelf --css tailwind --javascript esbuild
20
+ cd bookshelf
21
+ ```
22
+
23
+ ## 2. Add the gem
24
+
25
+ ```ruby
26
+ # Gemfile
27
+ gem "ruby_ui_admin"
28
+
29
+ # The admin's UI is built from your app's RubyUI components.
30
+ gem "ruby_ui", require: false
31
+
32
+ # Optional: renders the gem's docs at /admin/docs in development (see step 9).
33
+ group :development do
34
+ gem "kramdown"
35
+ gem "kramdown-parser-gfm"
36
+ end
37
+ ```
38
+
39
+ > Until it's published to RubyGems, point at a local checkout or Git instead, e.g.
40
+ > `gem "ruby_ui_admin", path: "../ruby_ui_admin"` or `gem "ruby_ui_admin", github: "you/ruby_ui_admin"`.
41
+
42
+ ```bash
43
+ bundle install
44
+ ```
45
+
46
+ Generate RubyUI and the components the admin uses (they land in `app/components/ruby_ui/*` and
47
+ their Stimulus controllers in `app/javascript/controllers/ruby_ui/*` — you own them).
48
+
49
+ > **Run `phlex:install` first.** RubyUI's installer only runs `phlex:install` (which creates
50
+ > `app/components/base.rb`) when `phlex-rails` isn't already in the bundle. Because `ruby_ui_admin`
51
+ > depends on `phlex-rails`, the installer skips that step, then aborts when it can't find
52
+ > `app/components/base.rb` — leaving `RubyUI::Base` ungenerated and breaking boot. Running
53
+ > `phlex:install` yourself first sidesteps it.
54
+
55
+ ```bash
56
+ bin/rails generate phlex:install # creates app/components/base.rb (see note above)
57
+ bin/rails generate ruby_ui:install
58
+ ```
59
+
60
+ > RubyUI's `install` overwrites your Tailwind entry CSS with an **interactive prompt** — run it on
61
+ > its own (don't paste a whole block at once) and answer `Y`, then re-add any customizations. On
62
+ > Tailwind v4 (the current default) that file is `app/assets/tailwind/application.css`; older setups
63
+ > use `app/assets/stylesheets/application.tailwind.css`. If the CSS build later errors on
64
+ > `tw-animate-css`, install it (`yarn add tw-animate-css` or pin it for importmap) and make sure your
65
+ > CSS imports it — RubyUI's installer adds this for you when it completes successfully.
66
+
67
+ Then generate the components the admin uses. The gem knows the set, so let it drive RubyUI's
68
+ generator (safe to re-run — existing components are skipped):
69
+
70
+ ```bash
71
+ bin/rails generate ruby_ui_admin:components
72
+ ```
73
+
74
+ > Under the hood it loops over the set the admin renders, one per command (RubyUI's generator takes
75
+ > one at a time), using the `Typography` group for `InlineLink` — a bare `ruby_ui:component
76
+ > InlineLink` fails silently and the index would break later. You can also run
77
+ > `bin/rails generate ruby_ui:component all` to generate everything.
78
+
79
+ ## 3. Generate two scaffolds with an association
80
+
81
+ `author:references` makes `Book` belong to an `Author` (adds the foreign key and the
82
+ `belongs_to :author`).
83
+
84
+ ```bash
85
+ bin/rails generate scaffold Author name:string email:string
86
+ bin/rails generate scaffold Book title:string published:boolean author:references
87
+ bin/rails db:migrate
88
+ ```
89
+
90
+ ## 4. Complete the association
91
+
92
+ The scaffold added `belongs_to :author` to `Book`. Add the other side to `Author`:
93
+
94
+ ```ruby
95
+ # app/models/author.rb
96
+ class Author < ApplicationRecord
97
+ has_many :books, dependent: :destroy
98
+
99
+ def to_label = name # used as the record's display label in the admin
100
+ end
101
+ ```
102
+
103
+ ```ruby
104
+ # app/models/book.rb
105
+ class Book < ApplicationRecord
106
+ belongs_to :author
107
+
108
+ def to_label = title
109
+ end
110
+ ```
111
+
112
+ ## 5. Mount and configure the admin
113
+
114
+ Run the install generator — it creates the initializer and mounts the engine at `/admin`:
115
+
116
+ ```bash
117
+ bin/rails generate ruby_ui_admin:install
118
+ ```
119
+
120
+ Then replace the generated initializer with this demo configuration (no auth, no policies):
121
+
122
+ ```ruby
123
+ # config/initializers/ruby_ui_admin.rb
124
+ RubyUIAdmin.configure do |config|
125
+ config.app_name = "Bookshelf Admin"
126
+ config.root_path = "/admin"
127
+ config.per_page = 20
128
+
129
+ # This demo has no authentication. Returning nil and disabling authorization lets the
130
+ # admin run without a current_user or any policies. In a real app, wire current_user_method
131
+ # to your auth (e.g. Devise) and set authorization_client = :action_policy + add policies.
132
+ config.current_user_method { nil }
133
+ config.authorization_client = nil
134
+
135
+ # REQUIRED — point the admin's <head> at your app's compiled CSS + JS (esbuild + Tailwind here).
136
+ config.head_assets = lambda do
137
+ safe_join([
138
+ stylesheet_link_tag("application", "data-turbo-track": "reload"),
139
+ javascript_include_tag("application", "data-turbo-track": "reload", defer: true)
140
+ ])
141
+ end
142
+ end
143
+ ```
144
+
145
+ Two more bits of wiring so the admin is styled and interactive (see
146
+ [Installation › Wire the assets](installation.md#3-wire-the-assets)):
147
+
148
+ - **Tailwind** — extract the admin's class inventory into your app and source it (the gem path
149
+ varies per environment, so don't hardcode it). Commit the generated file; re-run on gem upgrade:
150
+ ```bash
151
+ bin/rails ruby_ui_admin:tailwind_source # writes app/assets/tailwind/ruby_ui_admin_classes.html
152
+ ```
153
+ ```css
154
+ /* Tailwind v4: app/assets/tailwind/application.css (v3: app/assets/stylesheets/application.tailwind.css) */
155
+ @source "./ruby_ui_admin_classes.html";
156
+ ```
157
+ - **Stimulus** — copy the admin's controllers in with the generator, then add the one import it
158
+ prints to `app/javascript/application.js` (for esbuild here, that's `import "./ruby_ui_admin"`):
159
+ ```bash
160
+ bin/rails generate ruby_ui_admin:assets
161
+ ```
162
+ It brings the admin's own `rua--*` controllers **and** `ruby-ui--toaster`/`ruby-ui--toast` (the
163
+ admin uses RubyUI Toast for flash even though the component list above doesn't include it), all
164
+ registered under the right identifiers — no `stimulus:manifest:update` needed. Rebuild afterwards.
165
+ (On **Importmap + Propshaft** the generator pins the engine-served entrypoint instead of copying —
166
+ see [Installation › Wire the assets](installation.md#3-wire-the-assets).)
167
+
168
+ The generator also added this line to your routes (mount it manually if you skipped the
169
+ generator):
170
+
171
+ ```ruby
172
+ # config/routes.rb
173
+ Rails.application.routes.draw do
174
+ mount_ruby_ui_admin at: "/admin"
175
+
176
+ # ...your scaffolded routes (resources :authors, :books) stay as they are
177
+ end
178
+ ```
179
+
180
+ ## 6. Define the resources
181
+
182
+ Resources live in `app/ruby_ui_admin/resources/` and are auto‑discovered — no registration
183
+ needed. The class name maps to the model (`Author` → `Author`).
184
+
185
+ ```ruby
186
+ # app/ruby_ui_admin/resources/author.rb
187
+ module RubyUIAdmin
188
+ module Resources
189
+ class Author < RubyUIAdmin::BaseResource
190
+ self.title = :name # display label for an Author record
191
+
192
+ def fields
193
+ field :id, as: :id
194
+ field :name, as: :text, link_to_record: true, sortable: true
195
+ field :email, as: :text
196
+ field :books, as: :has_many # listed on the Author's show page
197
+ end
198
+ end
199
+ end
200
+ end
201
+ ```
202
+
203
+ ```ruby
204
+ # app/ruby_ui_admin/resources/book.rb
205
+ module RubyUIAdmin
206
+ module Resources
207
+ class Book < RubyUIAdmin::BaseResource
208
+ self.title = :title
209
+ self.includes = [:author] # eager-load to avoid N+1 on the index
210
+
211
+ def filters
212
+ filter RubyUIAdmin::Filters::PublishedFilter
213
+ filter RubyUIAdmin::Filters::AuthorFilter
214
+ end
215
+
216
+ def fields
217
+ field :title, as: :text, link_to_record: true, sortable: true
218
+ field :published, as: :boolean
219
+ field :author, as: :belongs_to # dropdown on forms, link on show/index
220
+ field :created_at, as: :date_time, only_on: %i[index show]
221
+ end
222
+ end
223
+ end
224
+ end
225
+ ```
226
+
227
+ ## 7. Add a couple of filters
228
+
229
+ Filters live in `app/ruby_ui_admin/filters/` and render above the index table.
230
+
231
+ ```ruby
232
+ # app/ruby_ui_admin/filters/published_filter.rb
233
+ module RubyUIAdmin
234
+ module Filters
235
+ class PublishedFilter < RubyUIAdmin::Filters::BooleanFilter
236
+ self.name = "Published"
237
+
238
+ def apply(_request, query, value)
239
+ query.where(published: value == "true")
240
+ end
241
+ end
242
+ end
243
+ end
244
+ ```
245
+
246
+ ```ruby
247
+ # app/ruby_ui_admin/filters/author_filter.rb
248
+ module RubyUIAdmin
249
+ module Filters
250
+ class AuthorFilter < RubyUIAdmin::Filters::SelectFilter
251
+ self.name = "Author"
252
+
253
+ # options is { value => label }.
254
+ def options
255
+ ::Author.order(:name).pluck(:id, :name).to_h
256
+ end
257
+
258
+ def apply(_request, query, value)
259
+ query.where(author_id: value)
260
+ end
261
+ end
262
+ end
263
+ end
264
+ ```
265
+
266
+ ## 8. Add some data (optional)
267
+
268
+ So the admin isn't empty:
269
+
270
+ ```ruby
271
+ # db/seeds.rb
272
+ rowling = Author.create!(name: "J. K. Rowling", email: "jk@example.com")
273
+ tolkien = Author.create!(name: "J. R. R. Tolkien", email: "jrr@example.com")
274
+
275
+ rowling.books.create!(title: "Harry Potter", published: true)
276
+ tolkien.books.create!(title: "The Hobbit", published: true)
277
+ tolkien.books.create!(title: "Unfinished Tales", published: false)
278
+ ```
279
+
280
+ ```bash
281
+ bin/rails db:seed
282
+ ```
283
+
284
+ ## 9. Run it
285
+
286
+ Use `bin/dev` (not `bin/rails server`) so the esbuild + Tailwind watchers build your assets:
287
+
288
+ ```bash
289
+ bin/dev
290
+ ```
291
+
292
+ Open <http://localhost:3000/admin>. You'll get a styled admin with
293
+ **Authors** and **Books** in the sidebar. On the Books index you'll see the **Published** and
294
+ **Author** filters; opening a Book shows its author as a link, and an Author shows its books.
295
+
296
+ > **Read the docs inside the app.** With the `kramdown` gems from step 2 installed, the admin serves
297
+ > this documentation at <http://localhost:3000/admin/docs> (development only, by default). Without
298
+ > those gems the page shows a notice telling you to add them. See
299
+ > [Configuration › In-app docs browser](configuration.md#in-app-docs-browser).
300
+
301
+ ---
302
+
303
+ ## Where to go next
304
+
305
+ - Group fields with [tabs & panels](../resources/tabs-panels.md), add [scopes](../resources/scopes.md)
306
+ and [index customization](../resources/index-customization.md).
307
+ - Add [custom actions](../actions/overview.md) (e.g. "Publish selected books").
308
+ - Wire real [authentication](authentication.md) and [authorization](../authorization/action-policy.md)
309
+ (set `authorization_client = :action_policy` and add policies).
310
+ - Browse the full [field catalog](../fields/overview.md) and [filter types](../filters/overview.md).
@@ -0,0 +1,66 @@
1
+ # Associations
2
+
3
+ Declare associations as fields. They render as links to the related records.
4
+
5
+ ```ruby
6
+ def fields
7
+ field :user, as: :belongs_to # parent record
8
+ field :profile, as: :has_one
9
+ field :comments, as: :has_many
10
+ field :tags, as: :has_and_belongs_to_many
11
+ end
12
+ ```
13
+
14
+ ## Display
15
+
16
+ | Type | Renders |
17
+ |---|---|
18
+ | `belongs_to` | a link to the parent record's show page; an editable select on forms |
19
+ | `has_one` | a link to the associated record's show page |
20
+ | `has_many` / `has_and_belongs_to_many` | a full-width **table** of the related records (capped, with “+N more”) |
21
+
22
+ Links resolve through the related model's registered resource (so a `Comment` row links to
23
+ `/admin/comments/:id`). If no resource is registered for the related model, the label is shown
24
+ without a link.
25
+
26
+ ### Collection tables (`has_many` / `has_and_belongs_to_many`)
27
+
28
+ Collection associations render as a RubyUI table whose **columns are the related resource's
29
+ index fields** (so a `comments` association shows the same columns as the Comments index), with
30
+ each row linking to the record's show page. On the show view the table spans the full width,
31
+ with the association name and description above it.
32
+
33
+ Limit and order the columns with the `fields:` option — a list of field ids from the related
34
+ resource:
35
+
36
+ ```ruby
37
+ field :suppliers, as: :has_many, fields: %i[name identifier state buyer]
38
+ ```
39
+
40
+ Without `fields:`, all of the related resource's index columns are shown. If the related model
41
+ has no registered resource, the table falls back to a single column of record links.
42
+
43
+ By default `has_one`/`has_many`/`has_and_belongs_to_many` appear **only on the show view**
44
+ (they're hidden on index and forms); `belongs_to` appears everywhere. Override with
45
+ `only_on:` / `hide_on:` like any field.
46
+
47
+ Give related models a `to_label` (or `name`/`title`) for friendly link text; otherwise a
48
+ `"<Model> #<id>"` label is used.
49
+
50
+ ## Authorization
51
+
52
+ Associations are gated like any field — define `view_<association>?` (or per-view
53
+ `show_<association>?`) on the resource policy:
54
+
55
+ ```ruby
56
+ class PostPolicy < RubyUIAdmin::BasePolicy
57
+ def view_comments? = user.admin? # hide the comments association from non-admins
58
+ end
59
+ ```
60
+
61
+ See [Field-level authorization](../authorization/field-authorization.md) for the full
62
+ resolution rules. As with any field, an undefined rule means the association stays visible.
63
+
64
+ > Inline association **management** (attach/detach controls and pagination within the table)
65
+ > is planned for a later phase. Today collection associations are read-only tables on the show
66
+ > view (capped, with a “+N more” note).
@@ -0,0 +1,68 @@
1
+ # Index customization
2
+
3
+ ## Custom header controls (`index_controls`)
4
+
5
+ Add your own buttons/links to the index header with `self.index_controls`, a block rendered
6
+ in the index view:
7
+
8
+ ```ruby
9
+ class Post < RubyUIAdmin::BaseResource
10
+ self.index_controls = -> { control_link("Invite buyer", invite_path) }
11
+ end
12
+ ```
13
+
14
+ The block is evaluated inside the index component, so it has access to:
15
+
16
+ - **`control_link(label, href, variant: :outline)`** — a button-styled link (`:outline` or
17
+ `:primary`). The common case.
18
+ - **`create_button(label: nil)`** — a primary "New" link for this resource.
19
+ - **Phlex element methods** (`a`, `button`, `div`, …) for full control.
20
+ - **Route helpers** via the mounted engine proxy, e.g. `ruby_ui_admin.resources_users_path`.
21
+ - **`@resource`** — the current resource instance.
22
+
23
+ ## Per-row controls (`row_controls`)
24
+
25
+ `self.row_controls = ->(record) { ... }` adds controls to each row's actions cell. The block
26
+ receives the row's `record` and is evaluated in the index component, so it has the same helpers
27
+ plus **`show_button(record, label: nil)`** and **`edit_button(record, label: nil)`**:
28
+
29
+ ```ruby
30
+ self.row_controls = ->(record) { show_button(record, label: "Preview") }
31
+
32
+ # Layout of the controls cell:
33
+ self.row_controls_config = { placement: :right, float: true, show_on_hover: true }
34
+ ```
35
+
36
+ Examples:
37
+
38
+ ```ruby
39
+ # Two controls, mixing the helper and raw Phlex:
40
+ self.index_controls = lambda do
41
+ control_link("Export CSV", "/exports/posts.csv")
42
+ a(href: ruby_ui_admin.resources_users_path, class: "text-sm text-primary hover:underline") { "Manage users" }
43
+ end
44
+ ```
45
+
46
+ Custom controls render alongside the built-in **New** button and any standalone actions.
47
+
48
+ > Use a `lambda`/`proc` with **no arguments** and read `@resource` directly inside the block.
49
+
50
+ ## Bulk selection
51
+
52
+ When a resource has bulk actions, the index shows per-row selection checkboxes (plus a
53
+ "select all"). `self.record_selector` (default `true`) controls this — set it to `false` to
54
+ hide the selection column even when bulk actions exist:
55
+
56
+ ```ruby
57
+ self.record_selector = false
58
+ ```
59
+
60
+ With no bulk actions, there's nothing to select, so no checkboxes render regardless.
61
+
62
+ ## Reserved options (no effect yet)
63
+
64
+ | Option | Intended purpose |
65
+ |---|---|
66
+ | `self.default_view_type` | Default index layout (`:table`, `:grid`, `:map`). Only `:table` is implemented. |
67
+
68
+ Setting it is harmless; it simply has no effect today.