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,50 @@
1
+ # Resources — Overview
2
+
3
+ A resource maps an ActiveRecord model to the admin UI. Resources live in
4
+ `app/ruby_ui_admin/resources` and are namespaced under `RubyUIAdmin::Resources`.
5
+
6
+ ```ruby
7
+ module RubyUIAdmin
8
+ module Resources
9
+ class Post < RubyUIAdmin::BaseResource
10
+ self.title = :title # attribute (or lambda) used as the record label
11
+ self.includes = [:user] # eager-loaded associations
12
+ self.model_class = Post # optional; inferred from the class name
13
+ self.index_query = -> { query.order(created_at: :desc) } # optional default scope
14
+ self.authorization_policy = RubyUIAdmin::Policies::PostPolicy
15
+
16
+ def fields
17
+ field :id, as: :id
18
+ field :title, as: :text, link_to_record: true, sortable: true
19
+ field :body, as: :text, only_on: %i[show new edit]
20
+ field :published, as: :boolean
21
+ field :user, as: :belongs_to
22
+ field :created_at, as: :date_time, only_on: %i[index show]
23
+ end
24
+ end
25
+ end
26
+ end
27
+ ```
28
+
29
+ ## Class options
30
+
31
+ | Option | Purpose |
32
+ |---|---|
33
+ | `self.title` | Attribute symbol or lambda used as the record's display label |
34
+ | `self.includes` | Associations to eager-load on the index query |
35
+ | `self.model_class` | Override the inferred model class |
36
+ | `self.index_query` | Lambda returning the default index scope (`query` is available) |
37
+ | `self.authorization_policy` | The action_policy policy class for this resource |
38
+ | `self.visible_on_sidebar` | Hide the resource from the sidebar when `false` |
39
+ | `self.description` | Subtitle shown under the index title |
40
+ | `self.record_selector` | Set `false` to hide the bulk-select checkbox column |
41
+ | `self.row_controls` | `->(record) { ... }` rendering extra per-row controls on the index (helpers: `show_button(record)`, `edit_button(record)`, `create_button`, `control_link`) |
42
+ | `self.row_controls_config` | Layout for the row-controls cell: `{ placement: :left/:right, float: true, show_on_hover: true }` |
43
+ | `self.countless` | Paginate without a `COUNT` query (pagy countless) — for very large tables. Can also be enabled globally via `config.pagination` |
44
+ | `self.remove_scope_all` | Hide the "All" scope tab (use with a default [scope](scopes.md)) |
45
+
46
+ ## Fields
47
+
48
+ `field :name, as: :type, **options` — see the [field catalog](../fields/overview.md) for all
49
+ types and options, and [tabs & panels](tabs-panels.md) for grouping. Fields can also be
50
+ auto-derived from the schema with `discover_columns` / `discover_associations`.
@@ -0,0 +1,54 @@
1
+ # Scopes
2
+
3
+ Named scopes partition a resource's index into tabs. Define them in
4
+ `app/ruby_ui_admin/scopes` under `RubyUIAdmin::Scopes`, then declare them on the resource.
5
+
6
+ ```ruby
7
+ module RubyUIAdmin
8
+ module Scopes
9
+ class Published < RubyUIAdmin::Scopes::BaseScope
10
+ self.name = "Published"
11
+ self.description = "Only published posts" # optional: tooltip on the scope tab
12
+ self.scope = -> { query.where(published: true) } # lambda, or...
13
+ # self.scope = :published # ...a Symbol = the model's named scope
14
+ self.default = true # optional: applied when no scope is selected
15
+ # self.visible = -> { user.admin? } # optional: show the tab conditionally
16
+ end
17
+ end
18
+ end
19
+ ```
20
+
21
+ Attach to a resource:
22
+
23
+ ```ruby
24
+ def scopes
25
+ scope RubyUIAdmin::Scopes::Published, default: true # mark default at attachment
26
+ scope RubyUIAdmin::Scopes::Drafts
27
+ end
28
+
29
+ # Hide the "All" tab (pair with a default scope):
30
+ self.remove_scope_all = true
31
+ ```
32
+
33
+ ## Behaviour
34
+
35
+ - The index renders a tab bar: **All** + one tab per scope.
36
+ - **`self.scope`** is a lambda (receiving `query` — the base relation, already narrowed by the
37
+ policy scope — and `params`) returning a narrowed relation, **or** a Symbol naming a model
38
+ scope/class method (e.g. `:published` → `Post.published`).
39
+ - Selecting a tab reloads the index with `?scope=<key>` (the key is the underscored class
40
+ name, e.g. `Published` → `published`). `?scope=all` clears any scope.
41
+ - **`self.default = true`** (or `scope Klass, default: true` at attachment) makes a scope apply
42
+ when no `?scope=` param is present; otherwise the index opens on **All**. The attachment option
43
+ wins over the class setting.
44
+ - **`self.remove_scope_all = true`** on the resource hides the **All** tab (use with a default scope).
45
+ - **`self.visible`** (a lambda) hides a scope's tab when it returns false.
46
+ - Scopes compose with the policy scope and [filters](../filters/overview.md): policy scope →
47
+ named scope → filters → sorting. The active scope is preserved when applying filters and
48
+ paginating.
49
+
50
+ ## Generator
51
+
52
+ ```bash
53
+ rails g ruby_ui_admin:scope Published
54
+ ```
@@ -0,0 +1,65 @@
1
+ # Tabs & Panels
2
+
3
+ Group fields on the show and form views with `panel`, `tabs` and `tab`.
4
+
5
+ ```ruby
6
+ def fields
7
+ field :id, as: :id
8
+
9
+ panel "Basic info" do
10
+ field :title, as: :text
11
+ field :status, as: :badge, options: {"draft" => :warning, "published" => :success}
12
+ end
13
+
14
+ tabs do
15
+ tab "Content", description: "The main content" do # description shown in the panel
16
+ field :body, as: :textarea
17
+ field :metadata, as: :key_value
18
+ end
19
+
20
+ tab "Relations" do
21
+ field :user, as: :belongs_to
22
+ field :comments, as: :has_many
23
+ end
24
+ end
25
+ end
26
+ ```
27
+
28
+ ## Behaviour
29
+
30
+ - **`panel(name = nil)`** — renders its fields inside a titled card. The name can also be
31
+ passed as a keyword (`panel name: "Basic info"`); both forms are equivalent.
32
+ - **`tabs`** — declares a group of tabs; only `tab` calls belong directly inside it.
33
+ - **`tab(name, description: nil)`** — a named section inside `tabs`; can contain fields and
34
+ panels. A bare field placed directly inside `tabs` (not wrapped in a `tab`) is treated as
35
+ an implicit single-field tab.
36
+ - Bare top-level fields (not inside a panel/tab) are grouped into a default card.
37
+ - Containers are pruned per view: a tab/panel whose fields are all hidden on a view
38
+ (via `only_on` / `hide_on`) isn't rendered there.
39
+ - The **index** view ignores grouping and shows the flattened, visible fields as columns.
40
+
41
+ ### Redundant-label de-duplication
42
+
43
+ To avoid showing the same word twice, two labels are suppressed automatically:
44
+
45
+ - A **tab `description:` that equals the tab name** isn't rendered (the tab bar already shows
46
+ the name).
47
+ - A **panel `name:` that equals the name of the tab containing it** isn't rendered (so a
48
+ `tab "Permissions" { panel name: "Permissions" { … } }` shows the title once, not twice).
49
+
50
+ Distinct descriptions and panel names still render normally.
51
+
52
+ ## Tab switching (JavaScript)
53
+
54
+ Tabs render as **stacked, titled cards without JavaScript**. With the bundled JS, the tab bar
55
+ is revealed, the redundant per-panel headings are hidden, and clicking a tab shows only that
56
+ panel (no server round-trip). See [JavaScript](../customization/javascript.md#tabs).
57
+
58
+ ### Lazy tab loading
59
+
60
+ With `config.lazy_tabs = true`, only the **first (active) tab** of each group renders up front;
61
+ the others fetch their content the first time they're opened (showing a spinner meanwhile),
62
+ so their fields and association queries only run on demand. This is handy for show pages with
63
+ several heavy association tabs. Without JS the active tab still renders and the deferred tabs
64
+ show a "requires JavaScript" notice. See
65
+ [JavaScript › Lazy tabs](../customization/javascript.md#lazy-tabs).
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module RubyUIAdmin
6
+ module Generators
7
+ class ActionGenerator < Rails::Generators::NamedBase
8
+ namespace "ruby_ui_admin:action"
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Generates a RubyUI Admin action."
12
+
13
+ class_option :standalone, type: :boolean, default: false,
14
+ desc: "Action does not require selected records"
15
+
16
+ def create_action
17
+ template "action.rb.tt", File.join("app/ruby_ui_admin/actions", "#{file_path}.rb")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Actions
5
+ class <%= class_name %> < RubyUIAdmin::BaseAction
6
+ self.name = "<%= human_name %>"
7
+ <% if options[:standalone] -%>
8
+ self.standalone = true
9
+ <% end -%>
10
+
11
+ def fields
12
+ # field :note, as: :text
13
+ end
14
+
15
+ def handle(query:, fields:, current_user:, **)
16
+ query.each do |record|
17
+ # do something with each record
18
+ end
19
+
20
+ succeed "Done!"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module RubyUIAdmin
6
+ module Generators
7
+ # Wires the admin's Stimulus controllers into the host app, tailored to its JS setup. The gem
8
+ # ships them flat (e.g. `rua--bulk-select_controller.js`) plus an `index.js` that imports and
9
+ # registers every one under the identifier its markup expects (`rua--bulk-select`,
10
+ # `ruby-ui--toaster`, …).
11
+ #
12
+ # rails g ruby_ui_admin:assets
13
+ #
14
+ # - **Bundler (esbuild/jsbundling/vite):** copies the whole directory verbatim into
15
+ # `app/javascript/ruby_ui_admin/` (a sibling of `controllers/`, so the host's
16
+ # `eagerLoadControllersFrom("controllers", …)` doesn't re-register them under bogus names).
17
+ # The bundler inlines `index.js`'s relative imports, so you just `import "./ruby_ui_admin"`.
18
+ # - **Importmap + Propshaft:** does NOT copy. Copied files get content-digested, and `index.js`'s
19
+ # relative `./x.js` imports resolve to the *undigested* URLs Propshaft won't serve → 404. Instead
20
+ # it pins the copy the engine already serves **undigested** via Rack::Static at
21
+ # `<ASSETS_MOUNT_PATH>/controllers/index.js`, where relative imports work and `@hotwired/stimulus`
22
+ # / `@hotwired/turbo` resolve through the host's own import map.
23
+ #
24
+ # Either way, no `stimulus:manifest:update` — the shipped `index.js` does the registration.
25
+ class AssetsGenerator < Rails::Generators::Base
26
+ namespace "ruby_ui_admin:assets"
27
+ source_root RubyUIAdmin::Engine.root.to_s
28
+
29
+ desc "Sets up the admin's Stimulus controllers (copies for bundlers; pins the served " \
30
+ "entrypoint for importmap)."
31
+
32
+ DEST = "app/javascript/ruby_ui_admin"
33
+ # The engine serves its controllers undigested here (see Engine's Rack::Static mount).
34
+ SERVED_INDEX = "#{RubyUIAdmin::ASSETS_MOUNT_PATH}/controllers/index.js"
35
+
36
+ def install
37
+ if importmap?
38
+ print_importmap_wiring
39
+ else
40
+ directory "public/ruby-ui-admin-assets/controllers", DEST
41
+ print_bundler_wiring
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def print_importmap_wiring
48
+ say "\nImportmap detected — nothing copied (copied files get digested and index.js's", :green
49
+ say "relative imports would 404 under Propshaft). The engine serves them undigested; pin that:", :green
50
+ say "\n # config/importmap.rb"
51
+ say %( pin "ruby_ui_admin", to: "#{SERVED_INDEX}"), :yellow
52
+ say "\n // app/javascript/application.js"
53
+ say %( import "ruby_ui_admin"), :yellow
54
+ say "\n(Stimulus + Turbo must be pinned too — stimulus-rails/turbo-rails do that already.)"
55
+ end
56
+
57
+ def print_bundler_wiring
58
+ say "\nCopied the admin's Stimulus controllers to #{DEST}/.", :green
59
+ say "Add this import to your JavaScript entrypoint so they're registered:", :yellow
60
+ say "\n // app/javascript/application.js"
61
+ say %( import "./ruby_ui_admin"), :yellow
62
+ say "\nThen rebuild your JS bundle. No stimulus:manifest:update needed — the copied"
63
+ say "index.js registers every controller under the right identifier."
64
+ end
65
+
66
+ def importmap?
67
+ File.exist?(File.join(destination_root, "config/importmap.rb"))
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module RubyUIAdmin
6
+ module Generators
7
+ class CardGenerator < Rails::Generators::NamedBase
8
+ namespace "ruby_ui_admin:card"
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Generates a RubyUI Admin card. Use --type metric|chart|partial."
12
+
13
+ class_option :type, type: :string, default: "metric",
14
+ desc: "Card type: metric, chart or partial"
15
+
16
+ def create_card
17
+ template "#{card_type}_card.rb.tt", File.join("app/ruby_ui_admin/cards", "#{file_path}.rb")
18
+ end
19
+
20
+ private
21
+
22
+ def card_type
23
+ type = options[:type].to_s
24
+ %w[metric chart partial].include?(type) ? type : "metric"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Cards
5
+ class <%= class_name %> < RubyUIAdmin::Cards::ChartCard
6
+ self.label = "<%= human_name %>"
7
+
8
+ def query
9
+ # e.g. Post.group(:status).count
10
+ {}
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Cards
5
+ class <%= class_name %> < RubyUIAdmin::Cards::MetricCard
6
+ self.label = "<%= human_name %>"
7
+
8
+ def query
9
+ # e.g. User.count
10
+ 0
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Cards
5
+ class <%= class_name %> < RubyUIAdmin::Cards::PartialCard
6
+ self.label = "<%= human_name %>"
7
+
8
+ def query
9
+ "<p class='text-sm text-muted-foreground'>Hello from <%= human_name %>.</p>"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module RubyUIAdmin
6
+ module Generators
7
+ # Generates the RubyUI components the admin's views render, by shelling out to
8
+ # `ruby_ui:component` once per component (RubyUI's generator takes one at a time). Without these,
9
+ # the admin references `RubyUI::*` constants your app hasn't generated and blows up with a
10
+ # NameError — this automates installing exactly the set it needs.
11
+ #
12
+ # rails g ruby_ui_admin:components
13
+ #
14
+ # Safe to re-run: passes `--skip`, so components you already have are left untouched.
15
+ class ComponentsGenerator < Rails::Generators::Base
16
+ namespace "ruby_ui_admin:components"
17
+
18
+ desc "Generates the RubyUI components the admin renders (via ruby_ui:component, one per command)."
19
+
20
+ # The RubyUI components the admin's views reference. These are RubyUI *group* names (what you
21
+ # pass to `ruby_ui:component`), not individual classes:
22
+ # - Typography provides InlineLink (there is no standalone `InlineLink` component).
23
+ # - Card/Table/Sidebar/Combobox each bring their whole family (CardHeader, TableRow, …).
24
+ # - Sheet isn't rendered directly — it's a dependency of RubyUI's mobile Sidebar.
25
+ # Kept in sync with the engine by test/generators/generators_test.rb.
26
+ COMPONENTS = %w[
27
+ Card Badge Button Checkbox Combobox Typography Link Input Textarea Table Sidebar Sheet
28
+ ].freeze
29
+
30
+ def generate_components
31
+ say "Generating the #{COMPONENTS.size} RubyUI components the admin needs " \
32
+ "(one ruby_ui:component call each)…", :green
33
+
34
+ COMPONENTS.each do |component|
35
+ run "bin/rails generate ruby_ui:component #{component} --skip"
36
+ end
37
+
38
+ say "\nDone. Re-run any time — existing components are skipped, not overwritten.", :green
39
+ say "If these fail, install RubyUI first (see docs/getting-started/installation.md).", :yellow
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module RubyUIAdmin
6
+ module Generators
7
+ # Generates a per-resource controller so you can override CRUD lifecycle hooks.
8
+ # rails g ruby_ui_admin:controller Buyer
9
+ class ControllerGenerator < Rails::Generators::NamedBase
10
+ namespace "ruby_ui_admin:controller"
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ desc "Generates a per-resource controller (subclass of ResourcesController)."
14
+
15
+ def create_controller
16
+ template "controller.rb.tt", File.join("app/controllers/ruby_ui_admin", "#{plural_file_name}_controller.rb")
17
+ end
18
+
19
+ private
20
+
21
+ def plural_file_name
22
+ file_name.pluralize
23
+ end
24
+
25
+ def controller_class_name
26
+ "#{class_name.pluralize}Controller"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ class <%= controller_class_name %> < RubyUIAdmin::ResourcesController
5
+ # Override CRUD lifecycle hooks as needed. Defaults live in ResourcesController.
6
+ #
7
+ # def after_create_path = resources_index_path
8
+ #
9
+ # def create_success_action
10
+ # super # or: redirect_to after_create_path, notice: "Created!"
11
+ # end
12
+ #
13
+ # def update_success_action
14
+ # redirect_to after_update_path, notice: "Updated!"
15
+ # end
16
+ #
17
+ # def destroy_fail_action
18
+ # redirect_to resources_index_path, alert: "Could not delete."
19
+ # end
20
+ #
21
+ # You can also override the CRUD actions themselves (create/update/destroy)
22
+ # and call `super`, or reimplement them.
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module RubyUIAdmin
6
+ module Generators
7
+ class DashboardGenerator < Rails::Generators::NamedBase
8
+ namespace "ruby_ui_admin:dashboard"
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Generates a RubyUI Admin dashboard."
12
+
13
+ def create_dashboard
14
+ template "dashboard.rb.tt", File.join("app/ruby_ui_admin/dashboards", "#{file_path}.rb")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Dashboards
5
+ class <%= class_name %> < RubyUIAdmin::BaseDashboard
6
+ self.name = "<%= human_name %>"
7
+ # self.grid_columns = 3
8
+
9
+ def cards
10
+ # card RubyUIAdmin::Cards::SomeMetric
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module RubyUIAdmin
6
+ module Generators
7
+ # Copies an engine controller / view component / UI primitive into the host app at
8
+ # the same path. The host's copy shadows the engine's (Zeitwerk loads the app's file
9
+ # first), so you can customize it freely.
10
+ #
11
+ # rails g ruby_ui_admin:eject --view index
12
+ # rails g ruby_ui_admin:eject --view layout
13
+ # rails g ruby_ui_admin:eject --controller resources
14
+ # rails g ruby_ui_admin:eject --ui button
15
+ class EjectGenerator < Rails::Generators::Base
16
+ namespace "ruby_ui_admin:eject"
17
+ source_root RubyUIAdmin::Engine.root.to_s
18
+
19
+ desc "Copies an engine file into your app so you can customize it."
20
+
21
+ class_option :controller, type: :string, desc: "Controller to eject (e.g. resources, application, actions)"
22
+ class_option :view, type: :string, desc: "View component to eject (e.g. index, show, form, layout)"
23
+ class_option :ui, type: :string, desc: "UI primitive to eject (e.g. button, card, table, badge)"
24
+
25
+ def eject
26
+ path = resolve_path
27
+ return if path.nil?
28
+
29
+ unless File.exist?(File.join(self.class.source_root, path))
30
+ say "RubyUI Admin: nothing to eject at #{path}", :red
31
+ return
32
+ end
33
+
34
+ copy_file path, path
35
+ say "\nEjected #{path}. Edit it in your app to customize.", :green
36
+ end
37
+
38
+ private
39
+
40
+ def resolve_path
41
+ if options[:controller]
42
+ "app/controllers/ruby_ui_admin/#{options[:controller]}_controller.rb"
43
+ elsif options[:view]
44
+ file = (options[:view] == "layout") ? "base" : options[:view]
45
+ "app/components/ruby_ui_admin/views/#{file}.rb"
46
+ elsif options[:ui]
47
+ "app/components/ruby_ui_admin/ui/#{options[:ui]}.rb"
48
+ else
49
+ say "Specify what to eject: --controller NAME, --view NAME (or layout), or --ui NAME", :yellow
50
+ nil
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module RubyUIAdmin
6
+ module Generators
7
+ class FilterGenerator < Rails::Generators::NamedBase
8
+ namespace "ruby_ui_admin:filter"
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Generates a RubyUI Admin filter. Use --type text|select|boolean."
12
+
13
+ class_option :type, type: :string, default: "text",
14
+ desc: "Filter type: text, select or boolean"
15
+
16
+ def create_filter
17
+ template "#{filter_type}_filter.rb.tt", File.join("app/ruby_ui_admin/filters", "#{file_path}_filter.rb")
18
+ end
19
+
20
+ private
21
+
22
+ def filter_type
23
+ type = options[:type].to_s
24
+ %w[text select boolean].include?(type) ? type : "text"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Filters
5
+ class <%= class_name %>Filter < RubyUIAdmin::Filters::BooleanFilter
6
+ self.name = "<%= human_name %>"
7
+
8
+ def apply(request, query, value)
9
+ query.where(<%= singular_name %>: value == "true")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Filters
5
+ class <%= class_name %>Filter < RubyUIAdmin::Filters::SelectFilter
6
+ self.name = "<%= human_name %>"
7
+
8
+ def apply(request, query, value)
9
+ query.where(<%= singular_name %>: value)
10
+ end
11
+
12
+ def options
13
+ {
14
+ # "value" => "Label"
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Filters
5
+ class <%= class_name %>Filter < RubyUIAdmin::Filters::TextFilter
6
+ self.name = "<%= human_name %>"
7
+
8
+ def apply(request, query, value)
9
+ query.where("<%= singular_name %> LIKE ?", "%#{value}%")
10
+ end
11
+ end
12
+ end
13
+ end