terrazzo 0.6.0 → 0.7.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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -0
  3. data/Rakefile +1 -0
  4. data/app/controllers/terrazzo/application_controller.rb +63 -14
  5. data/app/helpers/terrazzo/collection_actions_helper.rb +38 -13
  6. data/app/helpers/terrazzo/resource_paths_helper.rb +55 -0
  7. data/app/views/terrazzo/application/_edit_base.json.props +7 -4
  8. data/app/views/terrazzo/application/_index_base.json.props +83 -7
  9. data/app/views/terrazzo/application/_navigation.json.props +7 -4
  10. data/app/views/terrazzo/application/_new_base.json.props +6 -3
  11. data/app/views/terrazzo/application/_show_base.json.props +7 -5
  12. data/lib/generators/terrazzo/dashboard/dashboard_generator.rb +48 -24
  13. data/lib/generators/terrazzo/dashboard/templates/controller.rb.erb +8 -4
  14. data/lib/generators/terrazzo/dashboard/templates/dashboard.rb.erb +33 -25
  15. data/lib/generators/terrazzo/eject/eject_generator.rb +223 -113
  16. data/lib/generators/terrazzo/field/field_generator.rb +51 -7
  17. data/lib/generators/terrazzo/field/templates/FormField.jsx.erb +2 -3
  18. data/lib/generators/terrazzo/install/install_generator.rb +641 -4
  19. data/lib/generators/terrazzo/install/templates/admin.css +2 -0
  20. data/lib/generators/terrazzo/install/templates/application.js.erb +3 -0
  21. data/lib/generators/terrazzo/install/templates/components.json.erb +17 -0
  22. data/lib/generators/terrazzo/install/templates/custom_page_mapping.js.erb +5 -0
  23. data/lib/generators/terrazzo/install/templates/generated_page_mapping.js.erb +10 -0
  24. data/lib/generators/terrazzo/install/templates/jsconfig.json +8 -0
  25. data/lib/generators/terrazzo/install/templates/page_to_page_mapping.js.erb +9 -1
  26. data/lib/generators/terrazzo/install/templates/superglue.html.erb.erb +2 -2
  27. data/lib/generators/terrazzo/routes/routes_generator.rb +55 -20
  28. data/lib/generators/terrazzo/views/edit_generator.rb +16 -10
  29. data/lib/generators/terrazzo/views/eject_compatibility.rb +19 -0
  30. data/lib/generators/terrazzo/views/field_generator.rb +18 -6
  31. data/lib/generators/terrazzo/views/generated_defaults_helper.rb +110 -0
  32. data/lib/generators/terrazzo/views/index_generator.rb +9 -8
  33. data/lib/generators/terrazzo/views/layout_generator.rb +4 -3
  34. data/lib/generators/terrazzo/views/navigation_generator.rb +5 -1
  35. data/lib/generators/terrazzo/views/new_generator.rb +16 -10
  36. data/lib/generators/terrazzo/views/page_mapping_helper.rb +131 -8
  37. data/lib/generators/terrazzo/views/show_generator.rb +7 -6
  38. data/lib/generators/terrazzo/views/templates/components/CollectionFilters.jsx +25 -0
  39. data/lib/generators/terrazzo/views/templates/components/CollectionItemActions.jsx +68 -0
  40. data/lib/generators/terrazzo/views/templates/components/CollectionToolbarActions.jsx +60 -0
  41. data/lib/generators/terrazzo/views/templates/components/HasManyPagination.jsx +36 -0
  42. data/lib/generators/terrazzo/views/templates/components/Layout.jsx +9 -4
  43. data/lib/generators/terrazzo/views/templates/components/Pagination.jsx +32 -24
  44. data/lib/generators/terrazzo/views/templates/components/ResourceTable.jsx +172 -0
  45. data/lib/generators/terrazzo/views/templates/components/SearchBar.jsx +14 -6
  46. data/lib/generators/terrazzo/views/templates/components/SortableHeader.jsx +7 -5
  47. data/lib/generators/terrazzo/views/templates/components/app-sidebar.jsx +1 -1
  48. data/lib/generators/terrazzo/views/templates/components/site-header.jsx +2 -2
  49. data/lib/generators/terrazzo/views/templates/fields/asset/FormField.jsx +15 -0
  50. data/lib/generators/terrazzo/views/templates/fields/asset/IndexField.jsx +5 -0
  51. data/lib/generators/terrazzo/views/templates/fields/asset/ShowField.jsx +13 -0
  52. data/lib/generators/terrazzo/views/templates/fields/belongs_to/FormField.jsx +1 -1
  53. data/lib/generators/terrazzo/views/templates/fields/boolean/FormField.jsx +1 -1
  54. data/lib/generators/terrazzo/views/templates/fields/boolean/IndexField.jsx +1 -1
  55. data/lib/generators/terrazzo/views/templates/fields/has_many/FormField.jsx +3 -3
  56. data/lib/generators/terrazzo/views/templates/fields/has_many/IndexField.jsx +4 -3
  57. data/lib/generators/terrazzo/views/templates/fields/has_many/ShowField.jsx +21 -35
  58. data/lib/generators/terrazzo/views/templates/fields/has_one/FormField.jsx +1 -1
  59. data/lib/generators/terrazzo/views/templates/fields/hstore/FormField.jsx +5 -5
  60. data/lib/generators/terrazzo/views/templates/fields/hstore/IndexField.jsx +0 -2
  61. data/lib/generators/terrazzo/views/templates/fields/hstore/ShowField.jsx +1 -1
  62. data/lib/generators/terrazzo/views/templates/fields/number/IndexField.jsx +1 -1
  63. data/lib/generators/terrazzo/views/templates/fields/number/ShowField.jsx +1 -1
  64. data/lib/generators/terrazzo/views/templates/fields/polymorphic/FormField.jsx +3 -2
  65. data/lib/generators/terrazzo/views/templates/fields/rich_text/FormField.jsx +2 -2
  66. data/lib/generators/terrazzo/views/templates/fields/select/FormField.jsx +1 -1
  67. data/lib/generators/terrazzo/views/templates/fields/select/IndexField.jsx +1 -1
  68. data/lib/generators/terrazzo/views/templates/fields/shared/TextInputFormField.jsx +2 -2
  69. data/lib/generators/terrazzo/views/templates/fields/text/FormField.jsx +2 -2
  70. data/lib/generators/terrazzo/views/templates/pages/_collection.jsx +5 -3
  71. data/lib/generators/terrazzo/views/templates/pages/_form.jsx +37 -30
  72. data/lib/generators/terrazzo/views/templates/pages/_navigation.json.props +14 -4
  73. data/lib/generators/terrazzo/views/templates/pages/edit.jsx +6 -4
  74. data/lib/generators/terrazzo/views/templates/pages/index.jsx +20 -8
  75. data/lib/generators/terrazzo/views/templates/pages/new.jsx +8 -4
  76. data/lib/generators/terrazzo/views/templates/pages/show.jsx +30 -32
  77. data/lib/generators/terrazzo/views/views_generator.rb +9 -5
  78. data/lib/terrazzo/base_dashboard.rb +167 -5
  79. data/lib/terrazzo/csv_export.rb +38 -0
  80. data/lib/terrazzo/field/associative.rb +16 -4
  81. data/lib/terrazzo/field/base.rb +4 -0
  82. data/lib/terrazzo/field/belongs_to.rb +1 -2
  83. data/lib/terrazzo/field/deferred.rb +4 -0
  84. data/lib/terrazzo/field/has_many.rb +79 -11
  85. data/lib/terrazzo/field/password.rb +6 -0
  86. data/lib/terrazzo/field/rich_text.rb +13 -0
  87. data/lib/terrazzo/generator_helpers.rb +1 -1
  88. data/lib/terrazzo/has_many_pagination.rb +9 -1
  89. data/lib/terrazzo/namespace/resource.rb +30 -0
  90. data/lib/terrazzo/namespace.rb +26 -1
  91. data/lib/terrazzo/search.rb +29 -9
  92. data/lib/terrazzo/version.rb +1 -1
  93. data/lib/terrazzo.rb +1 -0
  94. data/terrazzo.gemspec +11 -1
  95. metadata +40 -6
  96. data/lib/generators/terrazzo/install/templates/application.json.props.erb +0 -17
  97. data/lib/generators/terrazzo/views/templates/fields/FieldRenderer.jsx +0 -103
  98. data/lib/generators/terrazzo/views/templates/fields/index.js +0 -81
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6030a4b97c39b154729b26996275571d8a6ffba864d84a0bff348257c308ce16
4
- data.tar.gz: 82edfaa39bd26353f4c56a2e96b22739620033d527d3c3fb57f6fcde45ec4b7c
3
+ metadata.gz: 0012b6331bb196e5dc5f21adf05701413e64d09464b0179e770351ca57d9fa4d
4
+ data.tar.gz: 2654d7e0cbb96d374b7b01eb9062608551a0f816d3eba202037eb03a383979e4
5
5
  SHA512:
6
- metadata.gz: cdc8552e4cf480b5dee699776ef5d1337eb7e1d92d387328d379b634165bf941774f4263ef0f0f8159ba8ae5fccb7d827b00425fed500b173f03325efddd908f
7
- data.tar.gz: c7ff22f7872258e527a0558ef9a54df140a481aa76f8fc906250b16fd7b1210dc7f1a8293a6677398888461e26dd5b82ad967c6463eefd44df3e3d312591c9e6
6
+ metadata.gz: 1c24716124c7d4aba7330a704feef1b978990a89b0c8f890145f1276453907bb52a22999e5776fce9f698df183e7d8bb820e90183da0cb9ff208b8700d0c2d75
7
+ data.tar.gz: 0ef677cdb49be947a26b5e97bdcd694474277b7e7f86d5328cef68312299bda1a44b7d0b49b7d0e3d8213c354906e0cf1464cf4fd2df6e0d147ffe56158b5312
data/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Terrazzo
2
+
3
+ A drop-in admin panel for Rails apps. Uses the [Administrate](https://github.com/thoughtbot/administrate) dashboard DSL with a React SPA frontend powered by [Superglue](https://github.com/thoughtbot/superglue).
4
+
5
+ - **Familiar DSL** — same `ATTRIBUTE_TYPES`, `COLLECTION_ATTRIBUTES`, `FORM_ATTRIBUTES` you already know
6
+ - **React SPA** — search, sort, and paginate without full page reloads, no separate API needed
7
+ - **shadcn/ui + Tailwind** — polished defaults with supported ejection when you want app-owned source
8
+ - **19 field types** — string, text, number, money, boolean, date/time, email, URL, password, select, rich text, hstore, asset, belongs_to, has_many, has_one, polymorphic, and more
9
+
10
+ ## Quick start
11
+
12
+ ```bash
13
+ # Add the gem
14
+ bundle add terrazzo
15
+
16
+ # Install Superglue (if not already set up)
17
+ rails g superglue:install
18
+
19
+ # Install Vite Rails for the default Vite setup.
20
+ bundle add vite_rails
21
+ bundle exec vite install
22
+ # Resolve any package-manager errors from Vite before continuing.
23
+
24
+ # Add the npm package and frontend dependencies
25
+ npm install terrazzo
26
+ npm install @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-dropdown-menu \
27
+ @radix-ui/react-label @radix-ui/react-popover @radix-ui/react-select \
28
+ @radix-ui/react-separator @radix-ui/react-slot @radix-ui/react-tooltip \
29
+ class-variance-authority lucide-react tailwindcss@latest
30
+ npm install --save-dev @tailwindcss/cli@latest # if your app does not already compile Tailwind
31
+
32
+ # Install Terrazzo — generates admin namespace, app barrels,
33
+ # shadcn metadata/path aliases, and dashboards.
34
+ # Uses Vite by default and requires vite_rails.
35
+ # For Rails esbuild pass --bundler=esbuild.
36
+ rails g terrazzo:install
37
+
38
+ # Make sure app/assets/stylesheets/admin.css is compiled by Tailwind.
39
+ # The installer adds build:admin:css when @tailwindcss/cli 4+ is installed,
40
+ # adds a package build script when one is missing, or warns with the setup
41
+ # command if no script or tailwindcss-rails setup compiles the Rails-linked
42
+ # admin stylesheet.
43
+
44
+ # Start Rails and Vite
45
+ bin/dev
46
+ ```
47
+
48
+ Visit `http://localhost:3000/admin` to see your admin panel.
49
+
50
+ ## Documentation
51
+
52
+ Full docs at **[gohypelab.github.io/terrazzo](https://gohypelab.github.io/terrazzo/)** — covers dashboards, fields, controllers, views, generators, and customization.
53
+
54
+ ## Requirements
55
+
56
+ - Ruby 3.1+
57
+ - Rails 7.1+
58
+ - Node.js 18+
59
+ - A JS bundler (Vite recommended, esbuild also supported)
60
+
61
+ ## Customizing Per-Row Actions
62
+
63
+ Terrazzo generates Show, Edit, and Destroy action buttons for each row on index pages and has_many tables on show pages.
64
+
65
+ Override `collection_item_actions` in the resource dashboard to add or replace actions per resource type. Use `super` when you want to keep the default Show/Edit/Destroy actions:
66
+
67
+ ```ruby
68
+ class OrderDashboard < Terrazzo::BaseDashboard
69
+ # ...
70
+
71
+ def collection_item_actions(resource, view)
72
+ super + [
73
+ { label: "Invoice", url: view.invoice_admin_order_path(resource) },
74
+ ]
75
+ end
76
+ end
77
+ ```
78
+
79
+ Return a new array instead when you want to fully replace the default actions.
80
+
81
+ The `view` argument gives access to route helpers. Each action hash supports `label` (String), `url` (String), `method` (optional, e.g. `"delete"`), `confirm` (optional confirmation message for non-GET form actions), and `sg_visit` (set to `false` to bypass SPA navigation).
82
+
83
+ ## Bulk Collection Actions
84
+
85
+ Override `collection_bulk_actions` to add actions for selected rows on an index page:
86
+
87
+ ```ruby
88
+ class OrderDashboard < Terrazzo::BaseDashboard
89
+ def collection_bulk_actions(view)
90
+ [
91
+ { label: "Mark shipped", url: view.bulk_ship_admin_orders_path, method: "post" },
92
+ ]
93
+ end
94
+ end
95
+ ```
96
+
97
+ Bulk action forms submit selected row IDs as `params[:ids]`. Define the matching collection route and controller action in your app.
98
+
99
+ ## License
100
+
101
+ MIT
data/Rakefile CHANGED
@@ -5,6 +5,7 @@ begin
5
5
  RSpec::Core::RakeTask.new(:spec) do |t|
6
6
  t.pattern = "spec/lib/**/*_spec.rb"
7
7
  end
8
+ task check: [:spec, :build]
8
9
  task default: :spec
9
10
  rescue LoadError
10
11
  # rspec not available
@@ -3,6 +3,7 @@ require "superglue"
3
3
  module Terrazzo
4
4
  class ApplicationController < ::ActionController::Base
5
5
  include Superglue::Controller
6
+ include Terrazzo::ResourcePathsHelper
6
7
 
7
8
  prepend_view_path(
8
9
  Superglue::Resolver.new(Terrazzo::Engine.root.join("app/views"))
@@ -16,26 +17,29 @@ module Terrazzo
16
17
 
17
18
  prepend Terrazzo::UsesSuperglue::TemplateLookupOverride
18
19
 
19
- helper_method :namespace, :dashboard, :resource_name, :resource_class, :application_title, :terrazzo_page_identifier, :route_exists?
20
+ helper_method :namespace, :dashboard, :resource_name, :resource_class, :application_title, :terrazzo_page_identifier, :route_exists?, :authorized_action?
20
21
  helper Terrazzo::CollectionActionsHelper
22
+ helper Terrazzo::ResourcePathsHelper
21
23
 
22
24
  def index
23
- search = Terrazzo::Search.new(scoped_resource, dashboard, params[:search])
24
- resources = search.run
25
+ resources, order = index_resources_and_order
25
26
 
26
- filter = Terrazzo::Filter.new(resources, dashboard, params[:filter], params[:filter_value])
27
- resources = filter.run
27
+ if request.format.csv?
28
+ return head :not_acceptable unless dashboard.csv_export_enabled?
28
29
 
29
- order = Terrazzo::Order.new(
30
- attribute: params[:order] || default_sorting_attribute,
31
- direction: params[:direction] || (params[:order] ? nil : default_sorting_direction)
32
- )
33
- resources = order.apply(resources, dashboard)
30
+ includes = dashboard.includes_for_attributes(dashboard.csv_attributes)
31
+ resources = resources.includes(*includes) if includes.any?
32
+
33
+ send_data Terrazzo::CsvExport.new(dashboard, resources).to_csv,
34
+ filename: dashboard.csv_filename,
35
+ type: "text/csv; charset=utf-8"
36
+ return
37
+ end
34
38
 
35
39
  includes = dashboard.collection_includes
36
40
  resources = resources.includes(*includes) if includes.any?
37
41
 
38
- @resources = resources.page(params[:_page]).per(params[:per_page] || 25)
42
+ @resources = resources.page(params[:_page]).per(index_per_page)
39
43
  @page = Terrazzo::Page::Collection.new(dashboard, resource_class, order: order)
40
44
  @search_term = params[:search]
41
45
  @active_filter = params[:filter]
@@ -44,6 +48,7 @@ module Terrazzo
44
48
 
45
49
  def show
46
50
  @resource = find_resource(params[:id])
51
+ authorize_action!(@resource, :show)
47
52
  @page = Terrazzo::Page::Show.new(
48
53
  dashboard, @resource,
49
54
  has_many_params: Terrazzo::HasManyPagination.extract(params, dashboard.has_many_attributes)
@@ -52,16 +57,20 @@ module Terrazzo
52
57
 
53
58
  def new
54
59
  @resource = resource_class.new
60
+ authorize_action!(@resource, :new)
55
61
  @page = Terrazzo::Page::Form.new(dashboard, @resource)
56
62
  end
57
63
 
58
64
  def edit
59
65
  @resource = find_resource(params[:id])
66
+ authorize_action!(@resource, :edit)
60
67
  @page = Terrazzo::Page::Form.new(dashboard, @resource)
61
68
  end
62
69
 
63
70
  def create
64
- @resource = resource_class.new(resource_params("create"))
71
+ @resource = resource_class.new
72
+ authorize_action!(@resource, :create)
73
+ @resource.assign_attributes(resource_params("create"))
65
74
  assign_has_one_associations(@resource)
66
75
 
67
76
  if @resource.save
@@ -76,6 +85,7 @@ module Terrazzo
76
85
 
77
86
  def update
78
87
  @resource = find_resource(params[:id])
88
+ authorize_action!(@resource, :update)
79
89
 
80
90
  rp = resource_params("update")
81
91
  assign_has_one_associations(@resource)
@@ -92,6 +102,7 @@ module Terrazzo
92
102
 
93
103
  def destroy
94
104
  @resource = find_resource(params[:id])
105
+ authorize_action!(@resource, :destroy)
95
106
  @resource.destroy
96
107
 
97
108
  redirect_to after_resource_destroyed_path,
@@ -113,12 +124,28 @@ module Terrazzo
113
124
  :asc
114
125
  end
115
126
 
127
+ def default_per_page
128
+ 25
129
+ end
130
+
131
+ def max_per_page
132
+ 100
133
+ end
134
+
135
+ def index_per_page
136
+ requested = params[:per_page].presence || default_per_page
137
+ requested = requested.to_i
138
+ requested = default_per_page if requested <= 0
139
+
140
+ [requested, max_per_page].min
141
+ end
142
+
116
143
  def after_resource_created_path(resource)
117
- [namespace, resource]
144
+ terrazzo_resource_member_path(resource) || after_resource_destroyed_path
118
145
  end
119
146
 
120
147
  def after_resource_updated_path(resource)
121
- [namespace, resource]
148
+ terrazzo_resource_member_path(resource) || after_resource_destroyed_path
122
149
  end
123
150
 
124
151
  def after_resource_destroyed_path
@@ -131,6 +158,22 @@ module Terrazzo
131
158
  resource_class.all
132
159
  end
133
160
 
161
+ def index_resources_and_order
162
+ search = Terrazzo::Search.new(scoped_resource, dashboard, params[:search])
163
+ resources = search.run
164
+
165
+ filter = Terrazzo::Filter.new(resources, dashboard, params[:filter], params[:filter_value])
166
+ resources = filter.run
167
+
168
+ order = Terrazzo::Order.new(
169
+ attribute: params[:order] || default_sorting_attribute,
170
+ direction: params[:direction] || (params[:order] ? nil : default_sorting_direction)
171
+ )
172
+ resources = order.apply(resources, dashboard)
173
+
174
+ [resources, order]
175
+ end
176
+
134
177
  def find_resource(id)
135
178
  @_find_resource ||= scoped_resource.find(id)
136
179
  end
@@ -178,6 +221,12 @@ module Terrazzo
178
221
  true
179
222
  end
180
223
 
224
+ def authorize_action!(resource, action)
225
+ return if authorized_action?(resource, action)
226
+
227
+ raise Terrazzo::NotAuthorizedError
228
+ end
229
+
181
230
  def namespace
182
231
  @_namespace ||= resolver.namespace
183
232
  end
@@ -2,20 +2,32 @@ module Terrazzo
2
2
  module CollectionActionsHelper
3
3
  def collection_item_actions(resource)
4
4
  resource_dashboard = "#{resource.class.name}Dashboard".safe_constantize&.new
5
- if resource_dashboard&.respond_to?(:collection_item_actions)
6
- resource_dashboard.collection_item_actions(resource, self)
7
- else
8
- default_collection_item_actions(resource)
9
- end
5
+ dashboard = resource_dashboard if resource_dashboard&.respond_to?(:collection_item_actions)
6
+ dashboard ||= Terrazzo::BaseDashboard.new
7
+ dashboard.collection_item_actions(resource, self)
8
+ end
9
+
10
+ def collection_action_path(resource, action = :show)
11
+ return nil unless collection_action_route?(resource, action)
12
+ return nil unless collection_action_authorized?(resource, action)
13
+
14
+ terrazzo_resource_member_path(resource, action: action)
10
15
  end
11
16
 
12
17
  def has_many_pagination_paths(field, resource)
13
18
  param_key = Terrazzo::HasManyPagination.param_key(field.attribute)
14
- base = request.query_parameters.merge(
19
+ legacy_param_key = Terrazzo::HasManyPagination.legacy_param_key(field.attribute)
20
+ query_parameters = request.query_parameters.except(
21
+ param_key,
22
+ param_key.to_sym,
23
+ legacy_param_key,
24
+ legacy_param_key.to_sym
25
+ )
26
+ base = query_parameters.merge(
15
27
  only_path: true,
16
28
  controller: controller_path,
17
29
  action: :show,
18
- id: resource.to_param,
30
+ id: terrazzo_resource_param(resource),
19
31
  format: nil,
20
32
  props_at: "data.attributes.#{field.attribute}"
21
33
  )
@@ -27,12 +39,25 @@ module Terrazzo
27
39
 
28
40
  private
29
41
 
30
- def default_collection_item_actions(resource)
31
- actions = []
32
- actions << { label: "Show", url: polymorphic_path([namespace, resource]) } rescue nil
33
- actions << { label: "Edit", url: edit_polymorphic_path([namespace, resource]) } rescue nil
34
- actions << { label: "Destroy", url: polymorphic_path([namespace, resource]), method: "delete", confirm: "Are you sure?" } rescue nil
35
- actions.compact
42
+ def collection_action_route?(resource, action)
43
+ path = terrazzo_resource_member_path(resource, action: action)
44
+ return false unless path
45
+
46
+ recognized = Rails.application.routes.recognize_path(path, method: collection_action_http_method(action))
47
+ recognized[:controller] == terrazzo_resource_controller_path(resource) &&
48
+ recognized[:action] == action.to_s
49
+ rescue ActionController::RoutingError, ActionController::UrlGenerationError, NoMethodError
50
+ false
51
+ end
52
+
53
+ def collection_action_http_method(action)
54
+ action == :destroy ? :delete : :get
55
+ end
56
+
57
+ def collection_action_authorized?(resource, action)
58
+ return true unless respond_to?(:authorized_action?, true)
59
+
60
+ authorized_action?(resource, action)
36
61
  end
37
62
  end
38
63
  end
@@ -0,0 +1,55 @@
1
+ module Terrazzo
2
+ module ResourcePathsHelper
3
+ def terrazzo_resource_collection_path(resource_or_class = resource_class)
4
+ url_for(
5
+ controller: terrazzo_resource_controller_path(resource_or_class),
6
+ action: :index,
7
+ only_path: true,
8
+ format: nil
9
+ )
10
+ rescue ActionController::UrlGenerationError
11
+ nil
12
+ end
13
+
14
+ def terrazzo_resource_new_path(resource_or_class = resource_class)
15
+ url_for(
16
+ controller: terrazzo_resource_controller_path(resource_or_class),
17
+ action: :new,
18
+ only_path: true,
19
+ format: nil
20
+ )
21
+ rescue ActionController::UrlGenerationError
22
+ nil
23
+ end
24
+
25
+ def terrazzo_resource_member_path(resource, action: :show)
26
+ return nil unless resource.respond_to?(:id) && resource.id.present?
27
+
28
+ route_action = action.to_sym == :destroy ? :show : action
29
+ url_for(
30
+ controller: terrazzo_resource_controller_path(resource),
31
+ action: route_action,
32
+ id: terrazzo_resource_param(resource),
33
+ only_path: true,
34
+ format: nil
35
+ )
36
+ rescue ActionController::UrlGenerationError
37
+ nil
38
+ end
39
+
40
+ # The URL parameter used to identify a member resource. Defaults to the
41
+ # record id. Override in a host app (e.g. to `resource.to_param`) to route
42
+ # members by slug or another identifier; the matching lookup belongs in the
43
+ # controller's #find_resource.
44
+ def terrazzo_resource_param(resource)
45
+ resource.id
46
+ end
47
+
48
+ def terrazzo_resource_controller_path(resource_or_class)
49
+ klass = resource_or_class.is_a?(Class) ? resource_or_class : resource_or_class.class
50
+ route_path = klass.model_name.name.underscore.pluralize
51
+
52
+ [namespace, route_path].compact.join("/")
53
+ end
54
+ end
55
+ end
@@ -4,7 +4,7 @@ json.pageTitle t("terrazzo.actions.edit", resource_name: resource_name)
4
4
 
5
5
  json.form do
6
6
  json.props({
7
- action: polymorphic_path([namespace, @resource]),
7
+ action: terrazzo_resource_member_path(@resource),
8
8
  method: "post",
9
9
  encType: "multipart/form-data"
10
10
  })
@@ -26,7 +26,8 @@ json.form do
26
26
  json.fields do
27
27
  json.array! @page.attributes("update") do |field|
28
28
  json.attribute field.attribute.to_s
29
- json.label field.attribute.to_s.humanize
29
+ json.label dashboard.attribute_label(field.attribute, :form)
30
+ json.hint dashboard.attribute_hint(field.attribute, :form)
30
31
  json.fieldType field.field_type
31
32
  json.value field.serialize_value(:form)
32
33
  json.options field.serializable_options(:form)
@@ -40,7 +41,8 @@ json.form do
40
41
  json.fields do
41
42
  json.array! group[:fields] do |field|
42
43
  json.attribute field.attribute.to_s
43
- json.label field.attribute.to_s.humanize
44
+ json.label dashboard.attribute_label(field.attribute, :form)
45
+ json.hint dashboard.attribute_hint(field.attribute, :form)
44
46
  json.fieldType field.field_type
45
47
  json.value field.serialize_value(:form)
46
48
  json.options field.serializable_options(:form)
@@ -54,7 +56,8 @@ end
54
56
 
55
57
  json.errors @resource.errors.full_messages
56
58
 
57
- json.showPath polymorphic_path([namespace, @resource])
59
+ json.layoutActions Array(dashboard.layout_actions(:edit, self, resource: @resource))
60
+ json.showPath route_exists?(:show) && authorized_action?(@resource, :show) ? terrazzo_resource_member_path(@resource) : nil
58
61
  json.indexPath begin
59
62
  url_for(controller: controller_path, action: :index, only_path: true)
60
63
  rescue ActionController::UrlGenerationError
@@ -1,27 +1,99 @@
1
+ per_page = @resources.limit_value
2
+
3
+ filter_url_for = ->(filter_name = nil) do
4
+ query = request.query_parameters.except("_page", "filter", "filter_value")
5
+ query["per_page"] = per_page
6
+ query["filter"] = filter_name if filter_name.present?
7
+ url_for(query.merge(only_path: true))
8
+ end
9
+
10
+ header_options_for = ->(attribute) do
11
+ options = (dashboard.collection_header_options(attribute) || {}).to_h
12
+ class_name = options.delete(:class_name) ||
13
+ options.delete("class_name") ||
14
+ options.delete(:className) ||
15
+ options.delete("className")
16
+
17
+ {
18
+ className: class_name,
19
+ meta: options.presence,
20
+ }.compact
21
+ end
22
+
23
+ cell_options_for = ->(attribute, resource) do
24
+ options = (dashboard.collection_cell_options(attribute, resource) || {}).to_h
25
+ class_name = options.delete(:class_name) ||
26
+ options.delete("class_name") ||
27
+ options.delete(:className) ||
28
+ options.delete("className")
29
+
30
+ {
31
+ className: class_name,
32
+ meta: options.presence,
33
+ }.compact
34
+ end
35
+
36
+ row_options_for = ->(resource) do
37
+ options = (dashboard.collection_row_options(resource) || {}).to_h
38
+ class_name = options.delete(:class_name) ||
39
+ options.delete("class_name") ||
40
+ options.delete(:className) ||
41
+ options.delete("className")
42
+
43
+ {
44
+ className: class_name,
45
+ meta: options.presence,
46
+ }.compact
47
+ end
48
+
1
49
  json.searchBar do
2
50
  json.searchTerm @search_term
3
51
  json.searchPath request.path
52
+ json.perPage per_page
53
+ end
54
+
55
+ json.layoutActions Array(dashboard.layout_actions(:index, self))
56
+ json.toolbarActions Array(dashboard.collection_toolbar_actions(self))
57
+ json.bulkActions Array(dashboard.collection_bulk_actions(self))
58
+
59
+ json.emptyState do
60
+ json.title "No #{resource_name.pluralize.downcase} found"
61
+ json.description dashboard.empty_collection_message
4
62
  end
5
63
 
6
64
  json.filters do
7
65
  json.available dashboard.collection_filters.keys.map(&:to_s)
8
66
  json.active @active_filter
9
67
  json.value @filter_value
68
+ json.allUrl filter_url_for.call
69
+ json.options do
70
+ json.array! dashboard.collection_filter_options(self) do |option|
71
+ value = (option[:value] || option["value"]).to_s
72
+ label = option[:label] || option["label"]
73
+ json.label label
74
+ json.value value
75
+ json.active @active_filter.to_s == value
76
+ json.url filter_url_for.call(value)
77
+ end
78
+ end
10
79
  end
11
80
 
12
81
  json.table do
13
82
  json.headers do
14
83
  json.array! @page.attribute_names do |attr|
15
84
  field_class = dashboard.attribute_type_for(attr)
16
- json.label attr.to_s.humanize
85
+ header_options = header_options_for.call(attr)
86
+ json.label dashboard.attribute_label(attr, :index)
17
87
  json.attribute attr.to_s
18
88
  json.sortable field_class.sortable?
19
89
  json.sortDirection @page.sort_direction_for(attr)
90
+ json.headerOptions header_options if header_options.present?
20
91
  json.sortUrl url_for(
21
92
  @page.order_params_for(attr).merge(
22
93
  search: @search_term,
23
94
  filter: @active_filter,
24
95
  filter_value: @filter_value,
96
+ per_page: per_page,
25
97
  _page: 1,
26
98
  only_path: true
27
99
  )
@@ -31,20 +103,24 @@ json.table do
31
103
 
32
104
  json.rows do
33
105
  json.array! @resources do |resource|
106
+ row_options = row_options_for.call(resource)
34
107
  json.id resource.id
35
- json.showPath polymorphic_path([namespace, resource]) rescue nil
108
+ json.showPath collection_action_path(resource, :show)
36
109
  json.collectionItemActions collection_item_actions(resource)
110
+ json.rowOptions row_options if row_options.present?
37
111
 
38
112
  json.cells do
39
113
  json.array! @page.attribute_names do |attr|
40
114
  field = dashboard.attribute_type_for(attr).new(attr, nil, :index, resource: resource)
115
+ cell_options = cell_options_for.call(attr, resource)
41
116
  json.attribute attr.to_s
42
117
  json.fieldType field.field_type
43
118
  json.value field.serialize_value(:index)
44
119
  json.options field.serializable_options
120
+ json.cellOptions cell_options if cell_options.present?
45
121
 
46
122
  if field.class.associative? && field.data.present?
47
- json.showPath polymorphic_path([namespace, field.data]) rescue nil
123
+ json.showPath collection_action_path(field.data, :show)
48
124
  end
49
125
  end
50
126
  end
@@ -56,11 +132,11 @@ json.pagination do
56
132
  json.currentPage @resources.current_page
57
133
  json.totalPages @resources.total_pages
58
134
  json.totalCount @resources.total_count
59
- json.perPage @resources.limit_value
135
+ json.perPage per_page
60
136
  if @resources.next_page
61
137
  json.nextPagePath url_for(
62
138
  _page: @resources.next_page,
63
- per_page: params[:per_page],
139
+ per_page: per_page,
64
140
  search: @search_term,
65
141
  order: params[:order],
66
142
  direction: params[:direction],
@@ -74,7 +150,7 @@ json.pagination do
74
150
  if @resources.prev_page
75
151
  json.prevPagePath url_for(
76
152
  _page: @resources.prev_page,
77
- per_page: params[:per_page],
153
+ per_page: per_page,
78
154
  search: @search_term,
79
155
  order: params[:order],
80
156
  direction: params[:direction],
@@ -89,4 +165,4 @@ end
89
165
 
90
166
  json.resourceName resource_name.pluralize
91
167
  json.singularResourceName resource_name
92
- json.newResourcePath route_exists?(:new) ? (new_polymorphic_path([namespace, resource_class]) rescue nil) : nil
168
+ json.newResourcePath route_exists?(:new) && authorized_action?(resource_class.new, :new) ? terrazzo_resource_new_path(resource_class) : nil
@@ -1,10 +1,13 @@
1
- resources = Terrazzo::Namespace.new(namespace).resources_with_index_route
1
+ resources = Terrazzo::Namespace.new(namespace).navigation_resources
2
+ navigation_groups = resources.group_by(&:navigation_group).map do |label, items|
3
+ { label: label, items: items }
4
+ end
2
5
 
3
- json.array! [{ label: "Resources", resources: resources }] do |group|
6
+ json.array! navigation_groups do |group|
4
7
  json.label group[:label]
5
8
  json.items do
6
- json.array! group[:resources] do |r|
7
- json.label r.resource_name.humanize.pluralize
9
+ json.array! group[:items] do |r|
10
+ json.label r.navigation_label
8
11
  json.path url_for(controller: "/#{r.controller_path}", action: :index, only_path: true)
9
12
  json.active r.controller_path == controller_path
10
13
  end
@@ -5,7 +5,7 @@ json.pageTitle t("terrazzo.actions.new", resource_name: resource_name)
5
5
 
6
6
  json.form do
7
7
  json.props({
8
- action: polymorphic_path([namespace, resource_class]),
8
+ action: terrazzo_resource_collection_path(resource_class),
9
9
  method: "post",
10
10
  encType: "multipart/form-data"
11
11
  })
@@ -22,7 +22,8 @@ json.form do
22
22
  json.fields do
23
23
  json.array! @page.attributes(action) do |field|
24
24
  json.attribute field.attribute.to_s
25
- json.label field.attribute.to_s.humanize
25
+ json.label dashboard.attribute_label(field.attribute, :form)
26
+ json.hint dashboard.attribute_hint(field.attribute, :form)
26
27
  json.fieldType field.field_type
27
28
  json.value field.serialize_value(:form)
28
29
  json.options field.serializable_options(:form)
@@ -36,7 +37,8 @@ json.form do
36
37
  json.fields do
37
38
  json.array! group[:fields] do |field|
38
39
  json.attribute field.attribute.to_s
39
- json.label field.attribute.to_s.humanize
40
+ json.label dashboard.attribute_label(field.attribute, :form)
41
+ json.hint dashboard.attribute_hint(field.attribute, :form)
40
42
  json.fieldType field.field_type
41
43
  json.value field.serialize_value(:form)
42
44
  json.options field.serializable_options(:form)
@@ -50,6 +52,7 @@ end
50
52
 
51
53
  json.errors @resource.errors.full_messages
52
54
 
55
+ json.layoutActions Array(dashboard.layout_actions(:new, self, resource: @resource))
53
56
  json.indexPath begin
54
57
  url_for(controller: controller_path, action: :index, only_path: true)
55
58
  rescue ActionController::UrlGenerationError