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.
- checksums.yaml +4 -4
- data/README.md +101 -0
- data/Rakefile +1 -0
- data/app/controllers/terrazzo/application_controller.rb +63 -14
- data/app/helpers/terrazzo/collection_actions_helper.rb +38 -13
- data/app/helpers/terrazzo/resource_paths_helper.rb +55 -0
- data/app/views/terrazzo/application/_edit_base.json.props +7 -4
- data/app/views/terrazzo/application/_index_base.json.props +83 -7
- data/app/views/terrazzo/application/_navigation.json.props +7 -4
- data/app/views/terrazzo/application/_new_base.json.props +6 -3
- data/app/views/terrazzo/application/_show_base.json.props +7 -5
- data/lib/generators/terrazzo/dashboard/dashboard_generator.rb +48 -24
- data/lib/generators/terrazzo/dashboard/templates/controller.rb.erb +8 -4
- data/lib/generators/terrazzo/dashboard/templates/dashboard.rb.erb +33 -25
- data/lib/generators/terrazzo/eject/eject_generator.rb +223 -113
- data/lib/generators/terrazzo/field/field_generator.rb +51 -7
- data/lib/generators/terrazzo/field/templates/FormField.jsx.erb +2 -3
- data/lib/generators/terrazzo/install/install_generator.rb +641 -4
- data/lib/generators/terrazzo/install/templates/admin.css +2 -0
- data/lib/generators/terrazzo/install/templates/application.js.erb +3 -0
- data/lib/generators/terrazzo/install/templates/components.json.erb +17 -0
- data/lib/generators/terrazzo/install/templates/custom_page_mapping.js.erb +5 -0
- data/lib/generators/terrazzo/install/templates/generated_page_mapping.js.erb +10 -0
- data/lib/generators/terrazzo/install/templates/jsconfig.json +8 -0
- data/lib/generators/terrazzo/install/templates/page_to_page_mapping.js.erb +9 -1
- data/lib/generators/terrazzo/install/templates/superglue.html.erb.erb +2 -2
- data/lib/generators/terrazzo/routes/routes_generator.rb +55 -20
- data/lib/generators/terrazzo/views/edit_generator.rb +16 -10
- data/lib/generators/terrazzo/views/eject_compatibility.rb +19 -0
- data/lib/generators/terrazzo/views/field_generator.rb +18 -6
- data/lib/generators/terrazzo/views/generated_defaults_helper.rb +110 -0
- data/lib/generators/terrazzo/views/index_generator.rb +9 -8
- data/lib/generators/terrazzo/views/layout_generator.rb +4 -3
- data/lib/generators/terrazzo/views/navigation_generator.rb +5 -1
- data/lib/generators/terrazzo/views/new_generator.rb +16 -10
- data/lib/generators/terrazzo/views/page_mapping_helper.rb +131 -8
- data/lib/generators/terrazzo/views/show_generator.rb +7 -6
- data/lib/generators/terrazzo/views/templates/components/CollectionFilters.jsx +25 -0
- data/lib/generators/terrazzo/views/templates/components/CollectionItemActions.jsx +68 -0
- data/lib/generators/terrazzo/views/templates/components/CollectionToolbarActions.jsx +60 -0
- data/lib/generators/terrazzo/views/templates/components/HasManyPagination.jsx +36 -0
- data/lib/generators/terrazzo/views/templates/components/Layout.jsx +9 -4
- data/lib/generators/terrazzo/views/templates/components/Pagination.jsx +32 -24
- data/lib/generators/terrazzo/views/templates/components/ResourceTable.jsx +172 -0
- data/lib/generators/terrazzo/views/templates/components/SearchBar.jsx +14 -6
- data/lib/generators/terrazzo/views/templates/components/SortableHeader.jsx +7 -5
- data/lib/generators/terrazzo/views/templates/components/app-sidebar.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/components/site-header.jsx +2 -2
- data/lib/generators/terrazzo/views/templates/fields/asset/FormField.jsx +15 -0
- data/lib/generators/terrazzo/views/templates/fields/asset/IndexField.jsx +5 -0
- data/lib/generators/terrazzo/views/templates/fields/asset/ShowField.jsx +13 -0
- data/lib/generators/terrazzo/views/templates/fields/belongs_to/FormField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/boolean/FormField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/boolean/IndexField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/has_many/FormField.jsx +3 -3
- data/lib/generators/terrazzo/views/templates/fields/has_many/IndexField.jsx +4 -3
- data/lib/generators/terrazzo/views/templates/fields/has_many/ShowField.jsx +21 -35
- data/lib/generators/terrazzo/views/templates/fields/has_one/FormField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/hstore/FormField.jsx +5 -5
- data/lib/generators/terrazzo/views/templates/fields/hstore/IndexField.jsx +0 -2
- data/lib/generators/terrazzo/views/templates/fields/hstore/ShowField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/number/IndexField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/number/ShowField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/polymorphic/FormField.jsx +3 -2
- data/lib/generators/terrazzo/views/templates/fields/rich_text/FormField.jsx +2 -2
- data/lib/generators/terrazzo/views/templates/fields/select/FormField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/select/IndexField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/shared/TextInputFormField.jsx +2 -2
- data/lib/generators/terrazzo/views/templates/fields/text/FormField.jsx +2 -2
- data/lib/generators/terrazzo/views/templates/pages/_collection.jsx +5 -3
- data/lib/generators/terrazzo/views/templates/pages/_form.jsx +37 -30
- data/lib/generators/terrazzo/views/templates/pages/_navigation.json.props +14 -4
- data/lib/generators/terrazzo/views/templates/pages/edit.jsx +6 -4
- data/lib/generators/terrazzo/views/templates/pages/index.jsx +20 -8
- data/lib/generators/terrazzo/views/templates/pages/new.jsx +8 -4
- data/lib/generators/terrazzo/views/templates/pages/show.jsx +30 -32
- data/lib/generators/terrazzo/views/views_generator.rb +9 -5
- data/lib/terrazzo/base_dashboard.rb +167 -5
- data/lib/terrazzo/csv_export.rb +38 -0
- data/lib/terrazzo/field/associative.rb +16 -4
- data/lib/terrazzo/field/base.rb +4 -0
- data/lib/terrazzo/field/belongs_to.rb +1 -2
- data/lib/terrazzo/field/deferred.rb +4 -0
- data/lib/terrazzo/field/has_many.rb +79 -11
- data/lib/terrazzo/field/password.rb +6 -0
- data/lib/terrazzo/field/rich_text.rb +13 -0
- data/lib/terrazzo/generator_helpers.rb +1 -1
- data/lib/terrazzo/has_many_pagination.rb +9 -1
- data/lib/terrazzo/namespace/resource.rb +30 -0
- data/lib/terrazzo/namespace.rb +26 -1
- data/lib/terrazzo/search.rb +29 -9
- data/lib/terrazzo/version.rb +1 -1
- data/lib/terrazzo.rb +1 -0
- data/terrazzo.gemspec +11 -1
- metadata +40 -6
- data/lib/generators/terrazzo/install/templates/application.json.props.erb +0 -17
- data/lib/generators/terrazzo/views/templates/fields/FieldRenderer.jsx +0 -103
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0012b6331bb196e5dc5f21adf05701413e64d09464b0179e770351ca57d9fa4d
|
|
4
|
+
data.tar.gz: 2654d7e0cbb96d374b7b01eb9062608551a0f816d3eba202037eb03a383979e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -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
|
-
|
|
24
|
-
resources = search.run
|
|
25
|
+
resources, order = index_resources_and_order
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
if request.format.csv?
|
|
28
|
+
return head :not_acceptable unless dashboard.csv_export_enabled?
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
144
|
+
terrazzo_resource_member_path(resource) || after_resource_destroyed_path
|
|
118
145
|
end
|
|
119
146
|
|
|
120
147
|
def after_resource_updated_path(resource)
|
|
121
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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) ?
|
|
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).
|
|
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!
|
|
6
|
+
json.array! navigation_groups do |group|
|
|
4
7
|
json.label group[:label]
|
|
5
8
|
json.items do
|
|
6
|
-
json.array! group[:
|
|
7
|
-
json.label r.
|
|
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:
|
|
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
|
|
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
|
|
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
|