terrazzo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/Rakefile +11 -0
  4. data/app/controllers/terrazzo/application_controller.rb +208 -0
  5. data/app/views/terrazzo/application/edit.json.props +70 -0
  6. data/app/views/terrazzo/application/index.json.props +97 -0
  7. data/app/views/terrazzo/application/new.json.props +65 -0
  8. data/app/views/terrazzo/application/show.json.props +44 -0
  9. data/app/views/terrazzo/application/superglue.html.erb +5 -0
  10. data/config/locales/en.yml +28 -0
  11. data/lib/generators/terrazzo/dashboard/dashboard_generator.rb +118 -0
  12. data/lib/generators/terrazzo/dashboard/templates/controller.rb.erb +4 -0
  13. data/lib/generators/terrazzo/dashboard/templates/dashboard.rb.erb +36 -0
  14. data/lib/generators/terrazzo/field/field_generator.rb +42 -0
  15. data/lib/generators/terrazzo/field/templates/FormField.jsx.erb +19 -0
  16. data/lib/generators/terrazzo/field/templates/IndexField.jsx.erb +5 -0
  17. data/lib/generators/terrazzo/field/templates/ShowField.jsx.erb +5 -0
  18. data/lib/generators/terrazzo/field/templates/field.rb.erb +23 -0
  19. data/lib/generators/terrazzo/install/install_generator.rb +85 -0
  20. data/lib/generators/terrazzo/install/templates/application.js.erb +27 -0
  21. data/lib/generators/terrazzo/install/templates/application.json.props +27 -0
  22. data/lib/generators/terrazzo/install/templates/application.json.props.erb +17 -0
  23. data/lib/generators/terrazzo/install/templates/application_controller.rb.erb +24 -0
  24. data/lib/generators/terrazzo/install/templates/application_visit.js.erb +8 -0
  25. data/lib/generators/terrazzo/install/templates/flash_slice.js.erb +42 -0
  26. data/lib/generators/terrazzo/install/templates/page_to_page_mapping.js.erb +22 -0
  27. data/lib/generators/terrazzo/install/templates/store.js.erb +24 -0
  28. data/lib/generators/terrazzo/install/templates/superglue.html.erb.erb +20 -0
  29. data/lib/generators/terrazzo/routes/routes_generator.rb +38 -0
  30. data/lib/generators/terrazzo/views/edit_generator.rb +28 -0
  31. data/lib/generators/terrazzo/views/field_generator.rb +32 -0
  32. data/lib/generators/terrazzo/views/index_generator.rb +24 -0
  33. data/lib/generators/terrazzo/views/layout_generator.rb +26 -0
  34. data/lib/generators/terrazzo/views/navigation_generator.rb +24 -0
  35. data/lib/generators/terrazzo/views/new_generator.rb +28 -0
  36. data/lib/generators/terrazzo/views/show_generator.rb +24 -0
  37. data/lib/generators/terrazzo/views/templates/components/FlashMessages.jsx +26 -0
  38. data/lib/generators/terrazzo/views/templates/components/Layout.jsx +23 -0
  39. data/lib/generators/terrazzo/views/templates/components/Pagination.jsx +69 -0
  40. data/lib/generators/terrazzo/views/templates/components/SearchBar.jsx +35 -0
  41. data/lib/generators/terrazzo/views/templates/components/SortableHeader.jsx +29 -0
  42. data/lib/generators/terrazzo/views/templates/components/app-sidebar.jsx +62 -0
  43. data/lib/generators/terrazzo/views/templates/components/site-header.jsx +19 -0
  44. data/lib/generators/terrazzo/views/templates/components/ui/avatar.jsx +35 -0
  45. data/lib/generators/terrazzo/views/templates/components/ui/badge.jsx +34 -0
  46. data/lib/generators/terrazzo/views/templates/components/ui/button.jsx +47 -0
  47. data/lib/generators/terrazzo/views/templates/components/ui/card.jsx +50 -0
  48. data/lib/generators/terrazzo/views/templates/components/ui/dropdown-menu.jsx +155 -0
  49. data/lib/generators/terrazzo/views/templates/components/ui/field.jsx +28 -0
  50. data/lib/generators/terrazzo/views/templates/components/ui/index.js +106 -0
  51. data/lib/generators/terrazzo/views/templates/components/ui/input.jsx +19 -0
  52. data/lib/generators/terrazzo/views/templates/components/ui/label.jsx +16 -0
  53. data/lib/generators/terrazzo/views/templates/components/ui/pagination.jsx +85 -0
  54. data/lib/generators/terrazzo/views/templates/components/ui/popover.jsx +27 -0
  55. data/lib/generators/terrazzo/views/templates/components/ui/select.jsx +127 -0
  56. data/lib/generators/terrazzo/views/templates/components/ui/separator.jsx +23 -0
  57. data/lib/generators/terrazzo/views/templates/components/ui/sheet.jsx +109 -0
  58. data/lib/generators/terrazzo/views/templates/components/ui/sidebar.jsx +629 -0
  59. data/lib/generators/terrazzo/views/templates/components/ui/skeleton.jsx +10 -0
  60. data/lib/generators/terrazzo/views/templates/components/ui/table.jsx +83 -0
  61. data/lib/generators/terrazzo/views/templates/components/ui/textarea.jsx +18 -0
  62. data/lib/generators/terrazzo/views/templates/components/ui/tooltip.jsx +24 -0
  63. data/lib/generators/terrazzo/views/templates/fields/FieldRenderer.jsx +103 -0
  64. data/lib/generators/terrazzo/views/templates/fields/belongs_to/FormField.jsx +29 -0
  65. data/lib/generators/terrazzo/views/templates/fields/belongs_to/IndexField.jsx +7 -0
  66. data/lib/generators/terrazzo/views/templates/fields/belongs_to/ShowField.jsx +7 -0
  67. data/lib/generators/terrazzo/views/templates/fields/boolean/FormField.jsx +21 -0
  68. data/lib/generators/terrazzo/views/templates/fields/boolean/IndexField.jsx +12 -0
  69. data/lib/generators/terrazzo/views/templates/fields/boolean/ShowField.jsx +6 -0
  70. data/lib/generators/terrazzo/views/templates/fields/date/FormField.jsx +8 -0
  71. data/lib/generators/terrazzo/views/templates/fields/date/IndexField.jsx +7 -0
  72. data/lib/generators/terrazzo/views/templates/fields/date/ShowField.jsx +7 -0
  73. data/lib/generators/terrazzo/views/templates/fields/date_time/FormField.jsx +8 -0
  74. data/lib/generators/terrazzo/views/templates/fields/date_time/IndexField.jsx +7 -0
  75. data/lib/generators/terrazzo/views/templates/fields/date_time/ShowField.jsx +7 -0
  76. data/lib/generators/terrazzo/views/templates/fields/email/FormField.jsx +7 -0
  77. data/lib/generators/terrazzo/views/templates/fields/email/IndexField.jsx +10 -0
  78. data/lib/generators/terrazzo/views/templates/fields/email/ShowField.jsx +10 -0
  79. data/lib/generators/terrazzo/views/templates/fields/has_many/FormField.jsx +151 -0
  80. data/lib/generators/terrazzo/views/templates/fields/has_many/IndexField.jsx +8 -0
  81. data/lib/generators/terrazzo/views/templates/fields/has_many/ShowField.jsx +72 -0
  82. data/lib/generators/terrazzo/views/templates/fields/has_one/FormField.jsx +28 -0
  83. data/lib/generators/terrazzo/views/templates/fields/has_one/IndexField.jsx +7 -0
  84. data/lib/generators/terrazzo/views/templates/fields/has_one/ShowField.jsx +7 -0
  85. data/lib/generators/terrazzo/views/templates/fields/hstore/FormField.jsx +120 -0
  86. data/lib/generators/terrazzo/views/templates/fields/hstore/IndexField.jsx +15 -0
  87. data/lib/generators/terrazzo/views/templates/fields/hstore/ShowField.jsx +24 -0
  88. data/lib/generators/terrazzo/views/templates/fields/index.js +81 -0
  89. data/lib/generators/terrazzo/views/templates/fields/number/FormField.jsx +9 -0
  90. data/lib/generators/terrazzo/views/templates/fields/number/IndexField.jsx +9 -0
  91. data/lib/generators/terrazzo/views/templates/fields/number/ShowField.jsx +9 -0
  92. data/lib/generators/terrazzo/views/templates/fields/password/FormField.jsx +7 -0
  93. data/lib/generators/terrazzo/views/templates/fields/password/IndexField.jsx +6 -0
  94. data/lib/generators/terrazzo/views/templates/fields/password/ShowField.jsx +6 -0
  95. data/lib/generators/terrazzo/views/templates/fields/polymorphic/FormField.jsx +58 -0
  96. data/lib/generators/terrazzo/views/templates/fields/polymorphic/IndexField.jsx +7 -0
  97. data/lib/generators/terrazzo/views/templates/fields/polymorphic/ShowField.jsx +7 -0
  98. data/lib/generators/terrazzo/views/templates/fields/rich_text/FormField.jsx +21 -0
  99. data/lib/generators/terrazzo/views/templates/fields/rich_text/IndexField.jsx +8 -0
  100. data/lib/generators/terrazzo/views/templates/fields/rich_text/ShowField.jsx +6 -0
  101. data/lib/generators/terrazzo/views/templates/fields/select/FormField.jsx +29 -0
  102. data/lib/generators/terrazzo/views/templates/fields/select/IndexField.jsx +8 -0
  103. data/lib/generators/terrazzo/views/templates/fields/select/ShowField.jsx +5 -0
  104. data/lib/generators/terrazzo/views/templates/fields/shared/TextInputFormField.jsx +24 -0
  105. data/lib/generators/terrazzo/views/templates/fields/string/FormField.jsx +7 -0
  106. data/lib/generators/terrazzo/views/templates/fields/string/IndexField.jsx +5 -0
  107. data/lib/generators/terrazzo/views/templates/fields/string/ShowField.jsx +5 -0
  108. data/lib/generators/terrazzo/views/templates/fields/text/FormField.jsx +20 -0
  109. data/lib/generators/terrazzo/views/templates/fields/text/IndexField.jsx +5 -0
  110. data/lib/generators/terrazzo/views/templates/fields/text/ShowField.jsx +5 -0
  111. data/lib/generators/terrazzo/views/templates/fields/time/FormField.jsx +8 -0
  112. data/lib/generators/terrazzo/views/templates/fields/time/IndexField.jsx +7 -0
  113. data/lib/generators/terrazzo/views/templates/fields/time/ShowField.jsx +7 -0
  114. data/lib/generators/terrazzo/views/templates/fields/url/FormField.jsx +7 -0
  115. data/lib/generators/terrazzo/views/templates/fields/url/IndexField.jsx +10 -0
  116. data/lib/generators/terrazzo/views/templates/fields/url/ShowField.jsx +10 -0
  117. data/lib/generators/terrazzo/views/templates/pages/_form.jsx +76 -0
  118. data/lib/generators/terrazzo/views/templates/pages/edit.jsx +44 -0
  119. data/lib/generators/terrazzo/views/templates/pages/index.jsx +106 -0
  120. data/lib/generators/terrazzo/views/templates/pages/new.jsx +36 -0
  121. data/lib/generators/terrazzo/views/templates/pages/show.jsx +82 -0
  122. data/lib/generators/terrazzo/views/views_generator.rb +52 -0
  123. data/lib/terrazzo/base_dashboard.rb +88 -0
  124. data/lib/terrazzo/engine.rb +21 -0
  125. data/lib/terrazzo/field/associative.rb +56 -0
  126. data/lib/terrazzo/field/base.rb +114 -0
  127. data/lib/terrazzo/field/belongs_to.rb +53 -0
  128. data/lib/terrazzo/field/boolean.rb +9 -0
  129. data/lib/terrazzo/field/date.rb +10 -0
  130. data/lib/terrazzo/field/date_time.rb +10 -0
  131. data/lib/terrazzo/field/deferred.rb +50 -0
  132. data/lib/terrazzo/field/email.rb +15 -0
  133. data/lib/terrazzo/field/has_many.rb +98 -0
  134. data/lib/terrazzo/field/has_one.rb +33 -0
  135. data/lib/terrazzo/field/hstore.rb +37 -0
  136. data/lib/terrazzo/field/money.rb +33 -0
  137. data/lib/terrazzo/field/number.rb +17 -0
  138. data/lib/terrazzo/field/password.rb +16 -0
  139. data/lib/terrazzo/field/polymorphic.rb +36 -0
  140. data/lib/terrazzo/field/rich_text.rb +30 -0
  141. data/lib/terrazzo/field/select.rb +33 -0
  142. data/lib/terrazzo/field/string.rb +27 -0
  143. data/lib/terrazzo/field/text.rb +27 -0
  144. data/lib/terrazzo/field/time.rb +10 -0
  145. data/lib/terrazzo/field/url.rb +9 -0
  146. data/lib/terrazzo/filter.rb +26 -0
  147. data/lib/terrazzo/generator_helpers.rb +36 -0
  148. data/lib/terrazzo/namespace/resource.rb +39 -0
  149. data/lib/terrazzo/namespace.rb +34 -0
  150. data/lib/terrazzo/not_authorized_error.rb +4 -0
  151. data/lib/terrazzo/order.rb +71 -0
  152. data/lib/terrazzo/page/base.rb +12 -0
  153. data/lib/terrazzo/page/collection.rb +28 -0
  154. data/lib/terrazzo/page/form.rb +43 -0
  155. data/lib/terrazzo/page/show.rb +46 -0
  156. data/lib/terrazzo/resource_resolver.rb +40 -0
  157. data/lib/terrazzo/search.rb +56 -0
  158. data/lib/terrazzo/version.rb +3 -0
  159. data/lib/terrazzo.rb +47 -0
  160. data/terrazzo.gemspec +32 -0
  161. metadata +297 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0ff3b38b0cdec57570baa990aef1998197eba301cfa0c297d0619ff7d4003374
4
+ data.tar.gz: f0556e47ac6c647db017b2ebb0167a5f9585a5d3c9353bab740131fbd717b02b
5
+ SHA512:
6
+ metadata.gz: b46cb63e1fe4754a6938fec445f76aa90eba36f55c3fde38f08713db3e325f504b3fe985816625a07e5a8b77e7fe6d820c62f262dd20d443deaa6566111c75db
7
+ data.tar.gz: '0974a53199cdbdb29b8e90096429ee853df3bbeb5414cb60d1ef2dceca0c6356a7533921a3ab383a19b04bc408e08658e8f5e4d58a55f7dbfb5caa06454ad39f'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 SuperglueAdmin Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ begin
4
+ require "rspec/core/rake_task"
5
+ RSpec::Core::RakeTask.new(:spec) do |t|
6
+ t.pattern = "spec/lib/**/*_spec.rb"
7
+ end
8
+ task default: :spec
9
+ rescue LoadError
10
+ # rspec not available
11
+ end
@@ -0,0 +1,208 @@
1
+ require "superglue"
2
+
3
+ module Terrazzo
4
+ class ApplicationController < ::ActionController::Base
5
+ include Superglue::Controller
6
+
7
+ prepend_view_path(
8
+ Superglue::Resolver.new(Terrazzo::Engine.root.join("app/views"))
9
+ )
10
+
11
+ prepend_view_path(
12
+ Superglue::Resolver.new(Rails.root.join("app/views"))
13
+ )
14
+
15
+ before_action :use_jsx_rendering_defaults
16
+
17
+ helper_method :namespace, :dashboard, :resource_name, :resource_class, :application_title, :terrazzo_page_identifier
18
+
19
+ def index
20
+ search = Terrazzo::Search.new(scoped_resource, dashboard, params[:search])
21
+ resources = search.run
22
+
23
+ filter = Terrazzo::Filter.new(resources, dashboard, params[:filter], params[:filter_value])
24
+ resources = filter.run
25
+
26
+ order = Terrazzo::Order.new(
27
+ attribute: params[:order] || default_sorting_attribute,
28
+ direction: params[:direction] || (params[:order] ? nil : default_sorting_direction)
29
+ )
30
+ resources = order.apply(resources, dashboard)
31
+
32
+ includes = dashboard.collection_includes
33
+ resources = resources.includes(*includes) if includes.any?
34
+
35
+ @resources = resources.page(params[:_page]).per(params[:per_page] || 25)
36
+ @page = Terrazzo::Page::Collection.new(dashboard, resource_class, order: order)
37
+ @search_term = params[:search]
38
+ @active_filter = params[:filter]
39
+ @filter_value = params[:filter_value]
40
+ end
41
+
42
+ def show
43
+ @resource = find_resource(params[:id])
44
+ @page = Terrazzo::Page::Show.new(dashboard, @resource)
45
+ end
46
+
47
+ def new
48
+ @resource = resource_class.new
49
+ @page = Terrazzo::Page::Form.new(dashboard, @resource)
50
+ end
51
+
52
+ def edit
53
+ @resource = find_resource(params[:id])
54
+ @page = Terrazzo::Page::Form.new(dashboard, @resource)
55
+ end
56
+
57
+ def create
58
+ @resource = resource_class.new(resource_params("create"))
59
+ assign_has_one_associations(@resource)
60
+
61
+ if @resource.save
62
+ redirect_to after_resource_created_path(@resource),
63
+ notice: t("terrazzo.controllers.create.success",
64
+ resource_name: resource_name)
65
+ else
66
+ @page = Terrazzo::Page::Form.new(dashboard, @resource)
67
+ render :new, status: :unprocessable_entity
68
+ end
69
+ end
70
+
71
+ def update
72
+ @resource = find_resource(params[:id])
73
+
74
+ rp = resource_params("update")
75
+ assign_has_one_associations(@resource)
76
+
77
+ if @resource.update(rp)
78
+ redirect_to after_resource_updated_path(@resource),
79
+ notice: t("terrazzo.controllers.update.success",
80
+ resource_name: resource_name)
81
+ else
82
+ @page = Terrazzo::Page::Form.new(dashboard, @resource)
83
+ render :edit, status: :unprocessable_entity
84
+ end
85
+ end
86
+
87
+ def destroy
88
+ @resource = find_resource(params[:id])
89
+ @resource.destroy
90
+
91
+ redirect_to after_resource_destroyed_path,
92
+ notice: t("terrazzo.controllers.destroy.success",
93
+ resource_name: resource_name)
94
+ end
95
+
96
+ protected
97
+
98
+ def application_title
99
+ Rails.application.class.module_parent_name.titleize
100
+ end
101
+
102
+ def default_sorting_attribute
103
+ nil
104
+ end
105
+
106
+ def default_sorting_direction
107
+ :asc
108
+ end
109
+
110
+ def after_resource_created_path(resource)
111
+ [namespace, resource]
112
+ end
113
+
114
+ def after_resource_updated_path(resource)
115
+ [namespace, resource]
116
+ end
117
+
118
+ def after_resource_destroyed_path
119
+ url_for(action: :index, only_path: true)
120
+ rescue ActionController::UrlGenerationError
121
+ "/"
122
+ end
123
+
124
+ def scoped_resource
125
+ resource_class.all
126
+ end
127
+
128
+ def find_resource(id)
129
+ scoped_resource.find(id)
130
+ rescue ActiveRecord::RecordNotFound
131
+ # Support models that override to_param (e.g., slug-based URLs)
132
+ scoped_resource.find_by!(slug: id)
133
+ end
134
+
135
+ def resource_params(action = nil)
136
+ permitted = params.require(resource_class.model_name.param_key)
137
+ .permit(dashboard.permitted_attributes(action))
138
+
139
+ # Transform and extract special field params
140
+ @has_one_assignments = {}
141
+ dashboard.flatten_attributes(dashboard.form_attributes(action)).each do |attr|
142
+ field_type = dashboard.attribute_type_for(attr)
143
+ klass = field_type.is_a?(Terrazzo::Field::Deferred) ? field_type.deferred_class : field_type
144
+
145
+ # Transform params (e.g., JSON string → Hash for hstore fields)
146
+ if permitted.key?(attr.to_s) && klass.respond_to?(:transform_param)
147
+ permitted[attr.to_s] = klass.transform_param(permitted[attr.to_s])
148
+ end
149
+
150
+ # Extract has_one virtual _id params since ActiveRecord doesn't
151
+ # natively support assigning has_one associations by id
152
+ next unless klass <= Terrazzo::Field::HasOne
153
+
154
+ id_key = "#{attr}_id"
155
+ if permitted.key?(id_key)
156
+ id_value = permitted.delete(id_key)
157
+ @has_one_assignments[attr] = id_value if id_value.present?
158
+ end
159
+ end
160
+
161
+ permitted
162
+ end
163
+
164
+ def assign_has_one_associations(resource)
165
+ return unless @has_one_assignments
166
+
167
+ @has_one_assignments.each do |attr, id_value|
168
+ reflection = resource.class.reflect_on_association(attr)
169
+ associated = reflection.klass.find(id_value)
170
+ resource.public_send(:"#{attr}=", associated)
171
+ end
172
+ end
173
+
174
+ def authorized_action?(resource, action)
175
+ true
176
+ end
177
+
178
+ def namespace
179
+ @_namespace ||= resolver.namespace
180
+ end
181
+
182
+ def dashboard
183
+ @_dashboard ||= resolver.dashboard_class.new
184
+ end
185
+
186
+ def resource_class
187
+ @_resource_class ||= resolver.resource_class
188
+ end
189
+
190
+ def resource_name
191
+ @_resource_name ||= resolver.resource_title
192
+ end
193
+
194
+ # Build a page identifier that matches the user's namespace, not the
195
+ # engine's internal template path. The React page-to-component mapping
196
+ # keys off this identifier (e.g. "admin/application/index").
197
+ def terrazzo_page_identifier
198
+ ns = controller_path.split("/").first
199
+ "#{ns}/application/#{action_name}"
200
+ end
201
+
202
+ private
203
+
204
+ def resolver
205
+ @_resolver ||= Terrazzo::ResourceResolver.new(controller_path)
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,70 @@
1
+ param_key = resource_class.model_name.param_key
2
+
3
+ json.pageTitle t("terrazzo.actions.edit", resource_name: resource_name)
4
+
5
+ json.form do
6
+ json.props({
7
+ action: polymorphic_path([namespace, @resource]),
8
+ method: "post"
9
+ })
10
+ json.extras do
11
+ if protect_against_forgery?
12
+ json.set!("authenticity_token") do
13
+ json.name "authenticity_token"
14
+ json.type "hidden"
15
+ json.defaultValue form_authenticity_token
16
+ json.autoComplete "off"
17
+ end
18
+ end
19
+ json.set!("_method") do
20
+ json.name "_method"
21
+ json.type "hidden"
22
+ json.value "patch"
23
+ end
24
+ end
25
+ json.fields do
26
+ json.array! @page.attributes("update") do |field|
27
+ json.attribute field.attribute.to_s
28
+ json.label field.attribute.to_s.humanize
29
+ json.fieldType field.field_type
30
+ json.value field.serialize_value(:form)
31
+ json.options field.serializable_options
32
+ json.required field.required?
33
+ json.input field.form_input_attributes(param_key)
34
+ end
35
+ end
36
+ json.fieldGroups do
37
+ json.array! @page.grouped_attributes("update") do |group|
38
+ json.name group[:name]
39
+ json.fields do
40
+ json.array! group[:fields] do |field|
41
+ json.attribute field.attribute.to_s
42
+ json.label field.attribute.to_s.humanize
43
+ json.fieldType field.field_type
44
+ json.value field.serialize_value(:form)
45
+ json.options field.serializable_options
46
+ json.required field.required?
47
+ json.input field.form_input_attributes(param_key)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ json.errors @resource.errors.full_messages
55
+
56
+ json.showPath polymorphic_path([namespace, @resource])
57
+ json.indexPath begin
58
+ url_for(controller: controller_path, action: :index, only_path: true)
59
+ rescue ActionController::UrlGenerationError
60
+ nil
61
+ end
62
+ json.resourceName resource_name
63
+
64
+ json.navigation do
65
+ json.array! Terrazzo::Namespace.new(namespace).resources_with_index_route do |nav_resource|
66
+ json.label nav_resource.resource_name.humanize.pluralize
67
+ json.path url_for(controller: "/#{nav_resource.controller_path}", action: :index, only_path: true)
68
+ json.active nav_resource.controller_path == controller_path
69
+ end
70
+ end
@@ -0,0 +1,97 @@
1
+ json.searchBar do
2
+ json.searchTerm @search_term
3
+ json.searchPath request.path
4
+ end
5
+
6
+ json.filters do
7
+ json.available dashboard.collection_filters.keys.map(&:to_s)
8
+ json.active @active_filter
9
+ json.value @filter_value
10
+ end
11
+
12
+ json.table do
13
+ json.headers do
14
+ json.array! @page.attribute_names do |attr|
15
+ field_class = dashboard.attribute_type_for(attr)
16
+ json.label attr.to_s.humanize
17
+ json.attribute attr.to_s
18
+ json.sortable field_class.sortable?
19
+ json.sortDirection @page.sort_direction_for(attr)
20
+ json.sortUrl url_for(
21
+ @page.order_params_for(attr).merge(
22
+ search: @search_term,
23
+ filter: @active_filter,
24
+ filter_value: @filter_value,
25
+ _page: 1,
26
+ only_path: true
27
+ )
28
+ )
29
+ end
30
+ end
31
+
32
+ json.rows do
33
+ json.array! @resources do |resource|
34
+ json.id resource.id
35
+ json.showPath polymorphic_path([namespace, resource]) rescue nil
36
+ json.editPath edit_polymorphic_path([namespace, resource]) rescue nil
37
+ json.deletePath polymorphic_path([namespace, resource]) rescue nil
38
+
39
+ json.cells do
40
+ json.array! @page.attribute_names do |attr|
41
+ field = dashboard.attribute_type_for(attr).new(attr, nil, :index, resource: resource)
42
+ json.attribute attr.to_s
43
+ json.fieldType field.field_type
44
+ json.value field.serialize_value(:index)
45
+ json.options field.serializable_options
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ json.pagination do
53
+ json.currentPage @resources.current_page
54
+ json.totalPages @resources.total_pages
55
+ json.totalCount @resources.total_count
56
+ json.perPage @resources.limit_value
57
+ if @resources.next_page
58
+ json.nextPagePath url_for(
59
+ _page: @resources.next_page,
60
+ search: @search_term,
61
+ order: params[:order],
62
+ direction: params[:direction],
63
+ filter: @active_filter,
64
+ filter_value: @filter_value,
65
+ props_at: "data.pagination",
66
+ only_path: true
67
+ )
68
+ else
69
+ json.nextPagePath nil
70
+ end
71
+ if @resources.prev_page
72
+ json.prevPagePath url_for(
73
+ _page: @resources.prev_page,
74
+ search: @search_term,
75
+ order: params[:order],
76
+ direction: params[:direction],
77
+ filter: @active_filter,
78
+ filter_value: @filter_value,
79
+ props_at: "data.pagination",
80
+ only_path: true
81
+ )
82
+ else
83
+ json.prevPagePath nil
84
+ end
85
+ end
86
+
87
+ json.resourceName resource_name.pluralize
88
+ json.singularResourceName resource_name
89
+ json.newResourcePath new_polymorphic_path([namespace, resource_class]) rescue nil
90
+
91
+ json.navigation do
92
+ json.array! Terrazzo::Namespace.new(namespace).resources_with_index_route do |nav_resource|
93
+ json.label nav_resource.resource_name.humanize.pluralize
94
+ json.path url_for(controller: "/#{nav_resource.controller_path}", action: :index, only_path: true)
95
+ json.active nav_resource.controller_path == controller_path
96
+ end
97
+ end
@@ -0,0 +1,65 @@
1
+ action = @resource.persisted? ? "update" : "create"
2
+ param_key = resource_class.model_name.param_key
3
+
4
+ json.pageTitle t("terrazzo.actions.new", resource_name: resource_name)
5
+
6
+ json.form do
7
+ json.props({
8
+ action: polymorphic_path([namespace, resource_class]),
9
+ method: "post"
10
+ })
11
+ json.extras do
12
+ if protect_against_forgery?
13
+ json.set!("authenticity_token") do
14
+ json.name "authenticity_token"
15
+ json.type "hidden"
16
+ json.defaultValue form_authenticity_token
17
+ json.autoComplete "off"
18
+ end
19
+ end
20
+ end
21
+ json.fields do
22
+ json.array! @page.attributes(action) do |field|
23
+ json.attribute field.attribute.to_s
24
+ json.label field.attribute.to_s.humanize
25
+ json.fieldType field.field_type
26
+ json.value field.serialize_value(:form)
27
+ json.options field.serializable_options
28
+ json.required field.required?
29
+ json.input field.form_input_attributes(param_key)
30
+ end
31
+ end
32
+ json.fieldGroups do
33
+ json.array! @page.grouped_attributes(action) do |group|
34
+ json.name group[:name]
35
+ json.fields do
36
+ json.array! group[:fields] do |field|
37
+ json.attribute field.attribute.to_s
38
+ json.label field.attribute.to_s.humanize
39
+ json.fieldType field.field_type
40
+ json.value field.serialize_value(:form)
41
+ json.options field.serializable_options
42
+ json.required field.required?
43
+ json.input field.form_input_attributes(param_key)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ json.errors @resource.errors.full_messages
51
+
52
+ json.indexPath begin
53
+ url_for(controller: controller_path, action: :index, only_path: true)
54
+ rescue ActionController::UrlGenerationError
55
+ nil
56
+ end
57
+ json.resourceName resource_name
58
+
59
+ json.navigation do
60
+ json.array! Terrazzo::Namespace.new(namespace).resources_with_index_route do |nav_resource|
61
+ json.label nav_resource.resource_name.humanize.pluralize
62
+ json.path url_for(controller: "/#{nav_resource.controller_path}", action: :index, only_path: true)
63
+ json.active nav_resource.controller_path == controller_path
64
+ end
65
+ end
@@ -0,0 +1,44 @@
1
+ json.pageTitle @page.page_title
2
+
3
+ json.attributeGroups do
4
+ json.array! @page.grouped_attributes do |group|
5
+ json.name group[:name]
6
+ json.attributes do
7
+ json.array! group[:fields] do |field|
8
+ json.attribute field.attribute.to_s
9
+ json.label field.attribute.to_s.humanize
10
+ json.fieldType field.field_type
11
+ json.value field.serialize_value(:show)
12
+ json.options field.serializable_options
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ json.attributes do
19
+ json.array! @page.attributes do |field|
20
+ json.attribute field.attribute.to_s
21
+ json.label field.attribute.to_s.humanize
22
+ json.fieldType field.field_type
23
+ json.value field.serialize_value(:show)
24
+ json.options field.serializable_options
25
+ end
26
+ end
27
+
28
+ json.editPath edit_polymorphic_path([namespace, @resource]) rescue nil
29
+ json.deletePath polymorphic_path([namespace, @resource]) rescue nil
30
+ json.indexPath begin
31
+ url_for(controller: controller_path, action: :index, only_path: true)
32
+ rescue ActionController::UrlGenerationError
33
+ nil
34
+ end
35
+ json.resourceName resource_name
36
+ json.pluralResourceName resource_name.pluralize
37
+
38
+ json.navigation do
39
+ json.array! Terrazzo::Namespace.new(namespace).resources_with_index_route do |nav_resource|
40
+ json.label nav_resource.resource_name.humanize.pluralize
41
+ json.path url_for(controller: "/#{nav_resource.controller_path}", action: :index, only_path: true)
42
+ json.active nav_resource.controller_path == controller_path
43
+ end
44
+ end
@@ -0,0 +1,5 @@
1
+ <script type="text/javascript">
2
+ window.SUPERGLUE_INITIAL_PAGE_STATE=<%= render_props %>;<%# erblint:disable ErbSafety %>
3
+ </script>
4
+
5
+ <div id="superglue-app"></div>
@@ -0,0 +1,28 @@
1
+ en:
2
+ terrazzo:
3
+ actions:
4
+ new: "New %{resource_name}"
5
+ edit: "Edit %{resource_name}"
6
+ show: "Show %{resource_name}"
7
+ destroy: "Delete %{resource_name}"
8
+ confirm_destroy: "Are you sure you want to delete this %{resource_name}?"
9
+ back: "Back"
10
+ save: "Save"
11
+ cancel: "Cancel"
12
+ controllers:
13
+ create:
14
+ success: "%{resource_name} was successfully created."
15
+ update:
16
+ success: "%{resource_name} was successfully updated."
17
+ destroy:
18
+ success: "%{resource_name} was successfully destroyed."
19
+ search:
20
+ placeholder: "Search %{resource_name}..."
21
+ clear: "Clear search"
22
+ pagination:
23
+ previous: "Previous"
24
+ next: "Next"
25
+ page_info: "Page %{current} of %{total}"
26
+ total_count: "%{count} total"
27
+ navigation:
28
+ title: "Admin"
@@ -0,0 +1,118 @@
1
+ require "rails/generators"
2
+ require "terrazzo/generator_helpers"
3
+
4
+ module Terrazzo
5
+ module Generators
6
+ class DashboardGenerator < Rails::Generators::NamedBase
7
+ include Terrazzo::GeneratorHelpers
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ class_option :namespace, type: :string, default: "admin",
12
+ desc: "Admin namespace"
13
+ class_option :bundler, type: :string, default: "vite",
14
+ desc: "JavaScript bundler (vite or sprockets)"
15
+
16
+ def create_dashboard
17
+ template "dashboard.rb.erb",
18
+ "app/dashboards/#{file_name}_dashboard.rb"
19
+ end
20
+
21
+ def create_controller
22
+ template "controller.rb.erb",
23
+ "app/controllers/#{options[:namespace]}/#{file_name.pluralize}_controller.rb"
24
+ end
25
+
26
+ def update_page_to_page_mapping
27
+ return if options[:bundler] == "vite"
28
+
29
+ mapping_path = "app/javascript/#{options[:namespace]}/page_to_page_mapping.js"
30
+ return unless File.exist?(mapping_path)
31
+
32
+ namespace_name = options[:namespace]
33
+ action_to_component = {
34
+ "index" => "AdminIndex",
35
+ "show" => "AdminShow",
36
+ "new" => "AdminNew",
37
+ "edit" => "AdminEdit"
38
+ }
39
+
40
+ action_to_component.each do |action, component|
41
+ key = "'#{namespace_name}/application/#{action}'"
42
+ mapping_path_content = File.read(mapping_path)
43
+ next if mapping_path_content.include?(key)
44
+
45
+ inject_into_file mapping_path, before: "}" do
46
+ " #{key}: #{component},\n"
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def model_class
54
+ class_name.constantize
55
+ end
56
+
57
+ def attribute_types
58
+ columns = model_class.columns.reject { |c| c.name == "id" }
59
+ associations = model_class.reflect_on_all_associations
60
+
61
+ types = {}
62
+
63
+ # ID first
64
+ types[:id] = "Field::Number"
65
+
66
+ # Columns
67
+ columns.each do |col|
68
+ next if col.name.end_with?("_type") && columns.any? { |c| c.name == col.name.sub(/_type$/, "_id") }
69
+ next if association_foreign_key?(col.name, associations)
70
+
71
+ types[col.name.to_sym] = column_to_field_type(col)
72
+ end
73
+
74
+ # Associations
75
+ associations.each do |assoc|
76
+ case assoc.macro
77
+ when :belongs_to
78
+ types[assoc.name] = "Field::BelongsTo"
79
+ when :has_many, :has_and_belongs_to_many
80
+ types[assoc.name] = "Field::HasMany"
81
+ when :has_one
82
+ types[assoc.name] = "Field::HasOne"
83
+ end
84
+ end
85
+
86
+ types
87
+ end
88
+
89
+ def collection_attributes
90
+ attrs = attribute_types.keys.reject { |a| %i[created_at updated_at].include?(a) }
91
+ attrs.first(4)
92
+ end
93
+
94
+ def show_page_attributes
95
+ attribute_types.keys
96
+ end
97
+
98
+ def form_attributes
99
+ attribute_types.keys.reject { |a| %i[id created_at updated_at].include?(a) }
100
+ end
101
+
102
+ def association_foreign_key?(column_name, associations)
103
+ associations.any? do |assoc|
104
+ assoc.macro == :belongs_to &&
105
+ assoc.foreign_key.to_s == column_name
106
+ end
107
+ end
108
+
109
+ def has_enum?(column_name)
110
+ model_class.defined_enums.key?(column_name.to_s)
111
+ end
112
+
113
+ def enum_collection(column_name)
114
+ model_class.defined_enums[column_name.to_s]&.keys || []
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,4 @@
1
+ module <%= options[:namespace].camelize %>
2
+ class <%= class_name.pluralize %>Controller < ApplicationController
3
+ end
4
+ end