flexi_admin 0.0.5 → 0.0.6

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +254 -18
  3. data/lib/flexi_admin/components/helpers/action_helper.rb +21 -0
  4. data/lib/flexi_admin/components/helpers/frontend_helper.rb +23 -0
  5. data/lib/flexi_admin/components/helpers/link_helper.rb +35 -2
  6. data/lib/flexi_admin/components/helpers/resource_helper.rb +79 -14
  7. data/lib/flexi_admin/components/helpers/url_helper.rb +15 -0
  8. data/lib/flexi_admin/components/nav/breadcrumbs_component.html.slim +13 -0
  9. data/lib/flexi_admin/components/nav/breadcrumbs_component.rb +86 -0
  10. data/lib/flexi_admin/components/resource/autocomplete_component.html.slim +19 -7
  11. data/lib/flexi_admin/components/resource/autocomplete_component.rb +41 -11
  12. data/lib/flexi_admin/components/resource/button_select_component.html.slim +2 -2
  13. data/lib/flexi_admin/components/resource/button_select_component.rb +8 -1
  14. data/lib/flexi_admin/components/resource/filter_component.html.slim +47 -0
  15. data/lib/flexi_admin/components/resource/filter_component.rb +51 -0
  16. data/lib/flexi_admin/components/resource/form_component.rb +16 -1
  17. data/lib/flexi_admin/components/resource/form_element_component.rb +1 -1
  18. data/lib/flexi_admin/components/resource/form_mixin.rb +186 -42
  19. data/lib/flexi_admin/components/resource/link_action_component.html.slim +2 -2
  20. data/lib/flexi_admin/components/resource/link_action_component.rb +3 -2
  21. data/lib/flexi_admin/components/resource/view_component.html.slim +14 -4
  22. data/lib/flexi_admin/components/resource/view_component.rb +11 -2
  23. data/lib/flexi_admin/components/resources/bulk_action/button_component.rb +8 -1
  24. data/lib/flexi_admin/components/resources/bulk_action/modal_component.html.slim +21 -15
  25. data/lib/flexi_admin/components/resources/bulk_action/modal_component.rb +16 -8
  26. data/lib/flexi_admin/components/resources/bulk_action/row_action_button_component.html.slim +16 -0
  27. data/lib/flexi_admin/components/resources/bulk_action/row_action_button_component.rb +78 -0
  28. data/lib/flexi_admin/components/resources/bulk_action/row_actions_dropdown_component.html.slim +15 -0
  29. data/lib/flexi_admin/components/resources/bulk_action/row_actions_dropdown_component.rb +33 -0
  30. data/lib/flexi_admin/components/resources/grid_view/card_component.html.slim +4 -1
  31. data/lib/flexi_admin/components/resources/grid_view/card_component.rb +4 -1
  32. data/lib/flexi_admin/components/resources/grid_view/grid_component.html.slim +3 -1
  33. data/lib/flexi_admin/components/resources/grid_view/grid_component.rb +4 -2
  34. data/lib/flexi_admin/components/resources/grid_view_component.rb +7 -2
  35. data/lib/flexi_admin/components/resources/index_page_component.html.slim +5 -4
  36. data/lib/flexi_admin/components/resources/index_page_component.rb +1 -0
  37. data/lib/flexi_admin/components/resources/list_view/table_component.html.slim +1 -1
  38. data/lib/flexi_admin/components/resources/list_view_component.rb +2 -1
  39. data/lib/flexi_admin/components/resources/pagination_component.html.slim +51 -39
  40. data/lib/flexi_admin/components/resources/pagination_component.rb +52 -47
  41. data/lib/flexi_admin/components/resources/switch_view_component.html.slim +2 -2
  42. data/lib/flexi_admin/components/resources/view_component.html.slim +15 -6
  43. data/lib/flexi_admin/components/shared/medium_component.html.slim +1 -1
  44. data/lib/flexi_admin/components/shared/table/header_item_component.html.slim +1 -1
  45. data/lib/flexi_admin/components/shared/table/header_item_component.rb +59 -61
  46. data/lib/flexi_admin/components/shared/tabs_component.html.slim +26 -0
  47. data/lib/flexi_admin/components/shared/tabs_component.rb +68 -0
  48. data/lib/flexi_admin/components.rb +11 -3
  49. data/lib/flexi_admin/config.rb +21 -3
  50. data/lib/flexi_admin/controllers/modals_controller.rb +4 -2
  51. data/lib/flexi_admin/controllers/resources_controller.rb +246 -51
  52. data/lib/flexi_admin/engine.rb +4 -0
  53. data/lib/flexi_admin/helpers/application_helper.rb +3 -0
  54. data/lib/flexi_admin/javascript/controllers/add_row_controller.js +118 -0
  55. data/lib/flexi_admin/javascript/controllers/autocomplete_controller.js +17 -1
  56. data/lib/flexi_admin/javascript/controllers/bulk_action_controller.js +192 -33
  57. data/lib/flexi_admin/javascript/controllers/copy_to_clipboard_controller.js +16 -0
  58. data/lib/flexi_admin/javascript/controllers/datalist_controller.js +1 -1
  59. data/lib/flexi_admin/javascript/controllers/delete_controller.js +40 -0
  60. data/lib/flexi_admin/javascript/controllers/expandable_field_controller.js +15 -0
  61. data/lib/flexi_admin/javascript/controllers/filter_auto_submit_controller.js +93 -0
  62. data/lib/flexi_admin/javascript/controllers/index.js +18 -0
  63. data/lib/flexi_admin/javascript/controllers/pagination_controller.js +8 -0
  64. data/lib/flexi_admin/javascript/controllers/tabs_controller.js +24 -0
  65. data/lib/flexi_admin/models/concerns/application_resource.rb +6 -0
  66. data/lib/flexi_admin/models/concerns/parentable.rb +3 -0
  67. data/lib/flexi_admin/models/context_params.rb +35 -10
  68. data/lib/flexi_admin/models/resources/context.rb +4 -0
  69. data/lib/flexi_admin/routes.rb +7 -1
  70. data/lib/flexi_admin/services/create_resource.rb +6 -6
  71. data/lib/flexi_admin/version.rb +1 -1
  72. data/lib/flexi_admin/views/shared/_reload_frame.slim +19 -0
  73. data/lib/flexi_admin.rb +0 -3
  74. metadata +171 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7899a2bba48f2ff003b6b90de70a6df1b61a05fc37e07dc47e07f4d3a3a9cc7
4
- data.tar.gz: c22ed4d803e860e4b250ac55670737c5df158705d3b5c3dd0d425646b4155adf
3
+ metadata.gz: 59b4913fe340c0507725c10bfdea39f47dcc271315c2d95e1c39469e6b44d56b
4
+ data.tar.gz: d45f9fdb404663bbe74a4e6afa103cf0d6c2018b6a346ee58b3f28ab55e19cd1
5
5
  SHA512:
6
- metadata.gz: 2d80c1451c28a1d842e2eb24660f5489184bb403a1eec4eb740eeda5f7eb11d3592ddd9a4cc9f80866664af28552aaaf2f70f07ddab18ca635fa551106f56f0e
7
- data.tar.gz: 7677d9ab7f7fdc1980066ec248e52725db1f4b017a1baaf0ae700cdb69f9cd06d81bdeee9271f51ab86617459aa278e433077f4cb56a65bea23f509e7bfb5f38
6
+ metadata.gz: d04fac65d08116d344bf99247c1c5b61c5bf5ba3fced0fd792516b87c7c37d5c1fc9b8570f0b0bcc4274f05489b414051246d184237b52369cf3aef9bc31efaa
7
+ data.tar.gz: 5a4d7d818963fabbe4554960b4d2e75c9880013f5fbe3dd5fdddd059f8b987c68e5de33572cd1dc494cb183ec16d049e24bc2ea1d5aceced9b7c49cca22e9ba8
data/README.md CHANGED
@@ -1,42 +1,278 @@
1
1
  # FlexiAdmin Rails
2
2
 
3
- Steps required to get FlexiAdmin working with a Rails app:
3
+ FlexiAdmin is a Rails engine that provides a flexible admin interface with modern JavaScript integration using Stimulus controllers and Turbo.
4
4
 
5
- **Gemfile**
5
+ ## Installation
6
6
 
7
- 1. add `gem 'flexi_admin', git: 'https://github.com/landovsky/flexi_admin.git', branch: 'main'` to the `Gemfile`
7
+ **Gemfile**
8
8
 
9
- **Stylesheets**
9
+ 1. Add to your `Gemfile`:
10
10
 
11
- 2. add `@import "flexi_admin.scss";` to the `app/assets/stylesheets/application.scss` file
11
+ ```ruby
12
+ gem 'flexi_admin', git: 'https://github.com/landovsky/flexi_admin.git', branch: 'main'
13
+ ```
12
14
 
13
- **JavaScript**
15
+ 2. Run `bundle install`
14
16
 
15
- 3. add `import "flexi_admin";` to the `app/javascript/application.js` file
17
+ ## Setup
16
18
 
17
- # Temporary (workaround) requirements
19
+ ### Stylesheets
18
20
 
19
- - can't get stylesheets to load from gem, so adding to host app:
21
+ Add to `app/assets/stylesheets/application.scss`:
20
22
 
21
23
  ```scss
22
- // Production
23
- // @import "flexi_admin.scss";
24
- // Local dev
25
- // @import "../../flexi_admin/app/assets/stylesheets/flexi_admin";
24
+ @import "flexi_admin.scss";
25
+ ```
26
+
27
+ ### JavaScript Integration
28
+
29
+ FlexiAdmin works with modern JavaScript bundlers. Choose your bundler below:
30
+
31
+ #### With esbuild (Recommended)
32
+
33
+ Add to `app/javascript/application.js`:
34
+
35
+ ```javascript
36
+ import "flexi_admin"
26
37
  ```
27
38
 
28
- - add modals controller:
39
+ **esbuild Configuration:**
40
+
41
+ The gem's Railtie automatically sets up NODE_PATH to include the FlexiAdmin JavaScript directory. If you're using a custom esbuild configuration, ensure it can resolve the gem's JavaScript:
42
+
43
+ ```javascript
44
+ // esbuild.config.mjs
45
+ import * as esbuild from 'esbuild'
46
+ import path from 'path'
47
+
48
+ await esbuild.build({
49
+ entryPoints: ['app/javascript/application.js'],
50
+ bundle: true,
51
+ sourcemap: true,
52
+ format: 'esm',
53
+ outdir: 'app/assets/builds',
54
+ publicPath: '/assets',
55
+ nodePaths: [
56
+ './node_modules',
57
+ // Path to gem JavaScript (adjust if needed)
58
+ path.resolve('../flexi_admin/lib/flexi_admin/javascript')
59
+ ]
60
+ })
61
+ ```
62
+
63
+ Or set NODE_PATH in your package.json build script:
64
+
65
+ ```json
66
+ {
67
+ "scripts": {
68
+ "build": "NODE_PATH=./node_modules:$(bundle show flexi_admin)/lib/flexi_admin/javascript esbuild app/javascript/*.* --bundle --sourcemap --format=esm --outdir=app/assets/builds"
69
+ }
70
+ }
71
+ ```
72
+
73
+ #### With webpack
74
+
75
+ Add to `app/javascript/application.js`:
29
76
 
77
+ ```javascript
78
+ import "flexi_admin"
30
79
  ```
31
- # frozen_string_literal: true
32
80
 
81
+ **Webpack Configuration:**
82
+
83
+ Update your webpack configuration to resolve the gem's JavaScript directory:
84
+
85
+ ```javascript
86
+ // webpack.config.js
87
+ const path = require('path')
88
+
89
+ module.exports = {
90
+ resolve: {
91
+ modules: [
92
+ path.resolve(__dirname, 'node_modules'),
93
+ // Add gem JavaScript directory
94
+ path.resolve(__dirname, '../flexi_admin/lib/flexi_admin/javascript')
95
+ ]
96
+ }
97
+ }
98
+ ```
99
+
100
+ Or find the gem path dynamically:
101
+
102
+ ```javascript
103
+ const { execSync } = require('child_process')
104
+ const gemPath = execSync('bundle show flexi_admin').toString().trim()
105
+
106
+ module.exports = {
107
+ resolve: {
108
+ modules: [
109
+ 'node_modules',
110
+ path.join(gemPath, 'lib/flexi_admin/javascript')
111
+ ]
112
+ }
113
+ }
114
+ ```
115
+
116
+ ### Stimulus Controllers
117
+
118
+ FlexiAdmin includes 19 Stimulus controllers that automatically register when you import the gem's JavaScript:
119
+
120
+ #### Available Controllers
121
+
122
+ - `add-row` - Dynamically add rows to forms
123
+ - `autocomplete` - Autocomplete search fields
124
+ - `bulk-action` - Bulk selection with persistence across pagination
125
+ - `button-select` - Custom button selection controls
126
+ - `datalist` - Enhanced datalist inputs
127
+ - `delete` - Confirmation dialogs for delete actions
128
+ - `filter-auto-submit` - Auto-submit filters on change
129
+ - `floating-toc` - Floating table of contents navigation
130
+ - `form` - Enhanced form behavior
131
+ - `form-validation` - Client-side form validation
132
+ - `pagination` - Page navigation controls
133
+ - `sorting` - Column sorting
134
+ - `switch-view` - Toggle between list/grid views
135
+ - `tabs` - Keeps the open `TabsComponent` tab in the URL hash
136
+ - `toast` - Toast notification system
137
+ - `trix` - Rich text editor integration
138
+ - `uploads` - File upload handling
139
+
140
+ #### Usage in Views
141
+
142
+ Use controllers in your ERB templates with the `data-controller` attribute:
143
+
144
+ ```erb
145
+ <div data-controller="flexi-admin--bulk-action"
146
+ data-flexi-admin--bulk-action-scope-value="users">
147
+ <!-- Your bulk action content -->
148
+ </div>
149
+ ```
150
+
151
+ Controller naming convention: `flexi-admin--[controller-name]`
152
+
153
+ #### Example: Pagination
154
+
155
+ ```erb
156
+ <nav data-controller="flexi-admin--pagination">
157
+ <%= link_to "Previous",
158
+ admin_users_path(page: @page - 1),
159
+ data: {
160
+ turbo_frame: "users_list",
161
+ action: "flexi-admin--pagination#navigate"
162
+ } %>
163
+ </nav>
164
+ ```
165
+
166
+ #### Example: Bulk Actions with Selection
167
+
168
+ ```erb
169
+ <div data-controller="flexi-admin--bulk-action"
170
+ data-flexi-admin--bulk-action-scope-value="users">
171
+
172
+ <%= check_box_tag "select_all", nil, false,
173
+ data: { action: "flexi-admin--bulk-action#toggleAll" } %>
174
+
175
+ <% @users.each do |user| %>
176
+ <%= check_box_tag "user_ids[]", user.id, false,
177
+ data: {
178
+ action: "flexi-admin--bulk-action#toggle",
179
+ flexi_admin__bulk_action_target: "checkbox"
180
+ } %>
181
+ <% end %>
182
+
183
+ <%= button_to "Delete Selected",
184
+ bulk_action_admin_users_path,
185
+ data: {
186
+ flexi_admin__bulk_action_target: "actionButton",
187
+ confirm: "Are you sure?"
188
+ } %>
189
+ </div>
190
+ ```
191
+
192
+ #### Example: Autocomplete Search
193
+
194
+ ```erb
195
+ <%= text_field_tag :search, params[:search],
196
+ data: {
197
+ controller: "flexi-admin--autocomplete",
198
+ flexi_admin__autocomplete_url_value: autocomplete_admin_users_path,
199
+ action: "input->flexi-admin--autocomplete#search"
200
+ } %>
201
+ ```
202
+
203
+ #### Example: Tabs on a show page
204
+
205
+ `FlexiAdmin::Components::Shared::TabsComponent` splits a long page into Bootstrap tabs. Every
206
+ pane is rendered up front (no lazy loading), so content behaves exactly as it did on one page.
207
+ The open tab is kept in the URL hash (`/orders/1#kalkulace`), so a reload or a shared link
208
+ reopens it.
209
+
210
+ ```slim
211
+ = render FlexiAdmin::Components::Shared::TabsComponent.new(id: 'order') do |tabs|
212
+ - tabs.with_tab(key: 'prehled', label: 'Přehled')
213
+ = render Admin::Order::Show::EditFormComponent.new(resource)
214
+ - tabs.with_tab(key: 'kalkulace', label: 'Kalkulace', count: calculations.size)
215
+ = render Admin::OrderCalculation::ResourcesComponent.new(calculations, context_params:, parent: resource)
216
+ ```
217
+
218
+ - `id:` prefixes the DOM ids (`order-tab-<key>`, `order-pane-<key>`). It must be unique on the page.
219
+ - `with_tab(key:, label:, count: nil, active: false)`: `count` renders a badge, and `active: true` opens that tab
220
+ (the first tab otherwise).
221
+ - `remember: false` turns off the URL-hash memory, for example for a second tab set on the same page.
222
+ - `nav_class:` sets the classes on the `ul.nav-tabs` (default `mb-4`).
223
+
224
+ Requires Bootstrap 5's JavaScript (`data-bs-toggle="tab"`).
225
+
226
+ ### Verification
227
+
228
+ To verify the JavaScript is loading correctly:
229
+
230
+ 1. Start your Rails server
231
+ 2. Open browser console
232
+ 3. Check for Stimulus registration:
233
+
234
+ ```javascript
235
+ // Should list all FlexiAdmin controllers
236
+ window.Stimulus.controllers
237
+ ```
238
+
239
+ ## Temporary Workarounds
240
+
241
+ Currently, the following manual steps are required:
242
+
243
+ ### Modals Controller
244
+
245
+ Create a modals controller in your app:
246
+
247
+ ```ruby
248
+ # app/controllers/modals_controller.rb
33
249
  class ModalsController < AdminController
34
250
  include FlexiAdmin::Controllers::ModalsController
35
251
  end
36
252
  ```
37
253
 
254
+ Add to routes:
255
+
38
256
  ```ruby
39
- resources :modals, only: [] do
40
- get :show, on: :collection
41
- end
257
+ resources :modals, only: [] do
258
+ get :show, on: :collection
259
+ end
42
260
  ```
261
+
262
+ ## Development
263
+
264
+ See [TESTING.md](TESTING.md) for information on:
265
+ - Running the test suite
266
+ - Setting up the dummy app for development
267
+ - JavaScript build process
268
+ - Test coverage
269
+
270
+ ## Documentation
271
+
272
+ - [TESTING.md](TESTING.md) - Testing guide and infrastructure
273
+ - [CI_REQUIREMENTS.md](CI_REQUIREMENTS.md) - CI/CD setup for esbuild
274
+ - [spec/dummy/README.md](spec/dummy/README.md) - Dummy app documentation
275
+
276
+ ## License
277
+
278
+ This project is licensed under the MIT License.
@@ -9,4 +9,25 @@ module FlexiAdmin::Components::Helpers::ActionHelper
9
9
  disabled:,
10
10
  selection_dependent:)
11
11
  end
12
+
13
+ def action_button(item, action_component, hide_icon: false, **options)
14
+ render FlexiAdmin::Components::Resources::BulkAction::RowActionButtonComponent.new(item,
15
+ context,
16
+ action_component,
17
+ hide_icon: hide_icon,
18
+ **options)
19
+ end
20
+
21
+ # Renders a dropdown with multiple action buttons for a single row/item
22
+ # Use this in column blocks when you have multiple actions for an item
23
+ # Example:
24
+ # = actions_dropdown(item) do |dropdown|
25
+ # - dropdown.with_action_button(item, context, ChangeStatus)
26
+ # - dropdown.with_action_button(item, context, AssignUser)
27
+ def actions_dropdown(item, &block)
28
+ render FlexiAdmin::Components::Resources::BulkAction::RowActionsDropdownComponent.new(item, context) do |component|
29
+ block.call(component) if block
30
+ component
31
+ end
32
+ end
12
33
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view"
4
+
5
+ module FlexiAdmin::Components::Helpers::FrontendHelper
6
+ def active(path, predicate = :ends_with)
7
+ css_match "active", request.path, match: path, use: predicate
8
+ end
9
+
10
+ # Example: css_match 'active', request.path, match: 'alternativy', use: :starts_with
11
+ def css_match(klasses, subject, match:, use: :includes)
12
+ case use
13
+ when :starts_with
14
+ klasses if subject.to_s.starts_with?(match)
15
+ when :ends_with
16
+ klasses if subject.to_s.ends_with?(match)
17
+ when :includes
18
+ klasses if subject.to_s.include?(match)
19
+ else
20
+ raise "Unknown predicate: #{predicate}"
21
+ end
22
+ end
23
+ end
@@ -2,7 +2,40 @@
2
2
 
3
3
  module FlexiAdmin::Components::Helpers::LinkHelper
4
4
  def navigate_to(title, resource)
5
- helpers.link_to title, [:admin, resource], "data-turbo-frame": "_top"
6
- # content_tag(:a, title, href: resource, "data-turbo-frame": "_top")
5
+ parent = context&.options&.dig(:parent) || parent_from_params
6
+ namespace = FlexiAdmin::Config.configuration.namespace
7
+ if resource.is_a?(String)
8
+ helpers.link_to title, resource, "data-turbo-frame": "_top"
9
+ else
10
+ path_segments = if parent.present? && parent.class != resource.class
11
+ namespace.present? ? [namespace.to_sym, parent, resource] : [parent, resource]
12
+ elsif namespace.present?
13
+ [namespace.to_sym, resource]
14
+ else
15
+ resource
16
+ end
17
+
18
+ result_path = begin
19
+ helpers.polymorphic_path(path_segments)
20
+ rescue NoMethodError
21
+ without_parent = path_segments.without(parent)
22
+ raise if without_parent.empty?
23
+
24
+ helpers.polymorphic_path(without_parent)
25
+ end
26
+
27
+ helpers.link_to title, result_path, "data-turbo-frame": "_top"
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def parent_from_params
34
+ return nil if context&.params&.parent.blank?
35
+
36
+ gid = URI.decode_www_form_component(context.params.parent)
37
+ GlobalID::Locator.locate(gid)
38
+ rescue StandardError
39
+ nil
7
40
  end
8
41
  end
@@ -3,14 +3,20 @@
3
3
  module FlexiAdmin::Components::Helpers::ResourceHelper
4
4
  include FlexiAdmin::Components::Helpers::UrlHelper
5
5
 
6
- def autocomplete_path(action:, fields:, parent: nil)
6
+ def autocomplete_path(action:, fields:, parent: nil, custom_scope: nil)
7
7
  payload = {
8
8
  ac_action: action,
9
9
  ac_path: resource__path,
10
10
  fa_parent: parent&.gid_param,
11
11
  ac_fields: fields
12
12
  }
13
- main_app.send("autocomplete_admin_#{scope_plural}_path", params: payload)
13
+
14
+ payload[:fa_custom_scope] = custom_scope if custom_scope.present?
15
+
16
+ path = namespaced_path("autocomplete", "namespace", scope_plural)
17
+ route_exists_in_main_app?(path) ? main_app.send(path, params: payload) : helpers.send(path, params: payload)
18
+ # rescue => e
19
+ # binding.pry if Rails.env.development?
14
20
  end
15
21
 
16
22
  def datalist_path(action:, fields:, parent: nil)
@@ -19,41 +25,99 @@ module FlexiAdmin::Components::Helpers::ResourceHelper
19
25
  ac_fields: fields
20
26
  }
21
27
 
22
- main_app.send("datalist_admin_#{scope_plural}_path", params: payload)
28
+ path = namespaced_path("datalist", "namespace", scope_plural)
29
+ route_exists_in_main_app?(path) ? main_app.send(path, params: payload) : helpers.send(path, params: payload)
30
+ # rescue => e
31
+ # binding.pry if Rails.env.development?
23
32
  end
24
33
 
25
34
  def edit_resource_path(resource, **params)
26
- main_app.send("edit_admin_#{scope_singular}_path", resource, params:)
35
+ path = namespaced_path("edit", "namespace", scope_singular)
36
+ if route_exists_in_main_app?(path)
37
+ main_app.send(path, resource,
38
+ params: params)
39
+ else
40
+ helpers.send(path, resource, params: params)
41
+ end
42
+ # rescue => e
43
+ # binding.pry if Rails.env.development?
27
44
  end
28
45
 
29
46
  def bulk_action_path(scope, **params)
30
- raise 'Scope is not defined' if scope.blank?
47
+ raise "Scope is not defined" if scope.blank?
31
48
 
32
- main_app.send("bulk_action_admin_#{scope}_path", params:)
49
+ path = namespaced_path("bulk_action", "namespace", scope)
50
+ route_exists_in_main_app?(path) ? main_app.send(path, params: params) : helpers.send(path, params: params)
51
+ # rescue => e
52
+ # binding.pry if Rails.env.development?
33
53
  end
34
54
 
35
55
  def resource_path(resource, **params)
36
- main_app.send("admin_#{scope_singular}_path", resource, params:)
56
+ path = namespaced_path("namespace", scope_singular)
57
+ if route_exists_in_main_app?(path)
58
+ main_app.send(path, resource,
59
+ params: params)
60
+ else
61
+ helpers.send(path, resource, params: params)
62
+ end
63
+ # rescue => e
64
+ # binding.pry if Rails.env.development?
37
65
  end
38
66
 
39
67
  def resource__path
40
- "admin_#{scope_singular}_path"
68
+ namespaced_path("namespace", scope_singular)
69
+ # rescue => e
70
+ # binding.pry if Rails.env.development?
41
71
  end
42
72
 
43
73
  def resources_path(**params)
44
- main_app.send("admin_#{scope_plural}_path", params:)
74
+ path = namespaced_path("namespace", scope_plural)
75
+ route_exists_in_main_app?(path) ? main_app.send(path, params: params) : helpers.send(path, params: params)
76
+ # rescue => e
77
+ # binding.pry if Rails.env.development?
45
78
  end
46
79
 
47
80
  def resource_input_name
48
81
  "#{scope_singular}_id"
82
+ # rescue => e
83
+ # binding.pry if Rails.env.development?
49
84
  end
50
85
 
51
86
  def scope_plural
52
- scope.gsub('/', '_')
87
+ if instance_variable_defined?(:@target_controller) && @target_controller.present?
88
+ @target_controller.to_s.underscore.gsub("/", "_")
89
+ elsif instance_variable_defined?(:@name) && @name.present?
90
+ field_name = @name.to_s.gsub(/_id$/, "")
91
+
92
+ base_name = field_name.gsub(/^(primary_|secondary_|main_|default_)/, "")
93
+
94
+ base_name.pluralize
95
+ else
96
+ unless defined?(scope) && !scope.is_a?(Proc)
97
+ raise "Cannot infer controller from scope without target_controller or field name"
98
+ end
99
+
100
+ scope.to_s.underscore.gsub("/", "_")
101
+
102
+ end
103
+ # rescue => e
104
+ # binding.pry if Rails.env.development?
53
105
  end
54
106
 
55
107
  def scope_singular
56
- scope.singularize
108
+ if scope.is_a?(Proc)
109
+ if instance_variable_defined?(:@target_controller) && @target_controller.present?
110
+ @target_controller.to_s.underscore.gsub("/", "_").singularize
111
+ elsif instance_variable_defined?(:@name) && @name.present?
112
+ @name.to_s.gsub(/_id$/, "").singularize
113
+ else
114
+ raise "Cannot infer singular scope from Proc without target_controller or field name"
115
+ end
116
+ else
117
+ scope.to_s.underscore.singularize
118
+ end
119
+ # rescue => e
120
+ # binding.pry if Rails.env.development?
57
121
  end
58
122
 
59
123
  def scope
@@ -61,11 +125,12 @@ module FlexiAdmin::Components::Helpers::ResourceHelper
61
125
  return context.scope if defined?(context)
62
126
  return resource.model_name.try(:plural) if defined?(resource)
63
127
 
64
- raise 'Scope is not defined'
128
+ raise "Scope is not defined"
65
129
  end
66
130
  end
67
131
 
68
- def paginate(resource, per_page: 10)
69
- resource.paginate(page: params[:page], per_page:)
132
+ def paginate(resource, per_page: WillPaginate.per_page)
133
+ resource.paginate(page: params[:page], per_page: per_page)
70
134
  end
135
+
71
136
  end
@@ -5,4 +5,19 @@ require "action_view"
5
5
  module FlexiAdmin::Components::Helpers::UrlHelper
6
6
  include ::ActionView::Helpers::UrlHelper
7
7
  include ::ActionView::RoutingUrlFor
8
+
9
+ def namespaced_path(*segments)
10
+ namespace = FlexiAdmin::Config.configuration.namespace
11
+
12
+ segments.map! do |segment|
13
+ segment = segment.to_s.gsub("/", "_")
14
+ segment == "namespace" ? (namespace.presence || nil) : segment
15
+ end
16
+ segments.compact!
17
+ "#{segments.join("_")}_path"
18
+ end
19
+
20
+ def route_exists_in_main_app?(path)
21
+ main_app.respond_to?(path)
22
+ end
8
23
  end
@@ -0,0 +1,13 @@
1
+ nav.breadcrumbs aria-label="breadcrumb"
2
+ ol.breadcrumb
3
+ - breadcrumbs.each_with_index do |crumb, idx|
4
+ - if idx < breadcrumbs.size - 1
5
+ li.breadcrumb-item
6
+ - if (main_app.routes.recognize_path("#{request.base_url}#{crumb[:url]}", method: :get) rescue false)
7
+ = helpers.link_to crumb[:label], crumb[:url], "data-turbo-frame": "_top"
8
+ - else
9
+ - puts "here"
10
+ = crumb[:label]
11
+ - else
12
+ li.breadcrumb-item.active aria-current="page"
13
+ = crumb[:label]
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlexiAdmin::Components::Nav
4
+ class BreadcrumbsComponent < FlexiAdmin::Components::BaseComponent
5
+ include FlexiAdmin::Components::Helpers::UrlHelper
6
+
7
+ attr_reader :path
8
+
9
+ def initialize(path:)
10
+ super()
11
+ @path = path
12
+ end
13
+
14
+ def breadcrumbs
15
+ segments = path.split("/").reject(&:blank?)
16
+ return [] if segments.size <= 1
17
+
18
+ crumbs = []
19
+ url = ""
20
+ segments.each_with_index do |segment, idx|
21
+ if idx.positive? && is_id?(segment)
22
+ # For instance, use singularized model name for lookup, but display uses instance title
23
+ model_class = infer_model_class(segments[idx - 1], segments[0..idx-2])
24
+ record = model_class&.find_by(id: segment)
25
+ label = record&.title if record.respond_to?(:title)
26
+ label ||= record&.try(:name)
27
+ label ||= "#{segments[idx - 1].singularize.camelize} ##{segment}"
28
+ url += "/#{segment}"
29
+ elsif !is_id?(segment)
30
+ # For collection, use plural segment for display and I18n lookup
31
+ plural_segment = segment
32
+ model_class = infer_model_class(segment, segments[0..idx-1])
33
+ label = breadcrumb_label(model_class, plural_segment)
34
+ url += "/#{segment}"
35
+ end
36
+
37
+ crumbs << { label: label, url: url.dup }
38
+ end
39
+ crumbs
40
+ end
41
+
42
+ private
43
+
44
+ def is_id?(segment)
45
+ segment.match?(/\A\d+\z/) || segment.match?(/\A[\da-fA-F-]{36}\z/)
46
+ end
47
+
48
+ def breadcrumb_label(model_class, plural_segment)
49
+ if model_class.is_a?(Class) && model_class.respond_to?(:breadcrumb_title)
50
+ model_class.breadcrumb_title
51
+ elsif model_class.is_a?(Class) && model_class.respond_to?(:model_name)
52
+ I18n.t("activerecord.models.#{model_class.model_name.i18n_key}.few",
53
+ default: model_class.model_name.human)
54
+ elsif model_class.is_a?(Module)
55
+ # For namespaces/modules, try to translate
56
+ module_name = model_class.name.underscore
57
+ I18n.t("activerecord.models.#{module_name}.few", default: module_name.humanize)
58
+ else
59
+ I18n.t("breadcrumbs.#{plural_segment}", default: plural_segment.titleize)
60
+ end
61
+ end
62
+
63
+ def infer_model_class(segment, previous_segments = [])
64
+ singular = segment.singularize
65
+ candidates = [singular.camelize]
66
+ if singular.include?('_')
67
+ parts = singular.split('_')
68
+ namespace = parts[0].camelize
69
+ model = parts[1..-1].join('_').camelize
70
+ candidates << "#{namespace}::#{model}"
71
+ end
72
+ # Try with previous segments as potential namespaces
73
+ previous_segments.reverse.each do |prev|
74
+ next if is_id?(prev)
75
+
76
+ namespace = prev.singularize.camelize
77
+ candidates << "#{namespace}::#{singular.camelize}"
78
+ end
79
+ candidates.each do |name|
80
+ cls = name.safe_constantize
81
+ return cls if cls.is_a?(Class) && cls < ActiveRecord::Base
82
+ end
83
+ nil
84
+ end
85
+ end
86
+ end