terrazzo 0.2.3 → 0.3.1

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/terrazzo/application_controller.rb +18 -2
  3. data/app/views/terrazzo/application/_navigation.json.props +12 -0
  4. data/app/views/terrazzo/application/edit.json.props +2 -9
  5. data/app/views/terrazzo/application/index.json.props +3 -11
  6. data/app/views/terrazzo/application/new.json.props +2 -9
  7. data/app/views/terrazzo/application/show.json.props +2 -10
  8. data/lib/generators/terrazzo/dashboard/dashboard_generator.rb +26 -0
  9. data/lib/generators/terrazzo/eject/eject_generator.rb +232 -0
  10. data/lib/generators/terrazzo/field/field_generator.rb +16 -9
  11. data/lib/generators/terrazzo/field/templates/FormField.jsx.erb +1 -2
  12. data/lib/generators/terrazzo/install/install_generator.rb +1 -1
  13. data/lib/generators/terrazzo/install/templates/admin.css +1 -0
  14. data/lib/generators/terrazzo/install/templates/{application.json.props → application.json.props.tt} +1 -0
  15. data/lib/generators/terrazzo/install/templates/page_to_page_mapping.js.erb +4 -15
  16. data/lib/generators/terrazzo/views/templates/components/Layout.jsx +1 -1
  17. data/lib/generators/terrazzo/views/templates/components/Pagination.jsx +3 -3
  18. data/lib/generators/terrazzo/views/templates/components/SearchBar.jsx +1 -1
  19. data/lib/generators/terrazzo/views/templates/components/SortableHeader.jsx +1 -1
  20. data/lib/generators/terrazzo/views/templates/components/app-sidebar.jsx +23 -22
  21. data/lib/generators/terrazzo/views/templates/components/site-header.jsx +2 -2
  22. data/lib/generators/terrazzo/views/templates/components/ui/index.js +1 -1
  23. data/lib/generators/terrazzo/views/templates/fields/belongs_to/FormField.jsx +1 -1
  24. data/lib/generators/terrazzo/views/templates/fields/boolean/FormField.jsx +1 -1
  25. data/lib/generators/terrazzo/views/templates/fields/boolean/IndexField.jsx +1 -1
  26. data/lib/generators/terrazzo/views/templates/fields/has_many/FormField.jsx +3 -3
  27. data/lib/generators/terrazzo/views/templates/fields/has_many/IndexField.jsx +1 -1
  28. data/lib/generators/terrazzo/views/templates/fields/has_many/ShowField.jsx +3 -3
  29. data/lib/generators/terrazzo/views/templates/fields/has_one/FormField.jsx +1 -1
  30. data/lib/generators/terrazzo/views/templates/fields/hstore/FormField.jsx +3 -3
  31. data/lib/generators/terrazzo/views/templates/fields/hstore/IndexField.jsx +1 -1
  32. data/lib/generators/terrazzo/views/templates/fields/hstore/ShowField.jsx +1 -1
  33. data/lib/generators/terrazzo/views/templates/fields/polymorphic/FormField.jsx +1 -1
  34. data/lib/generators/terrazzo/views/templates/fields/rich_text/FormField.jsx +2 -2
  35. data/lib/generators/terrazzo/views/templates/fields/select/FormField.jsx +1 -1
  36. data/lib/generators/terrazzo/views/templates/fields/select/IndexField.jsx +1 -1
  37. data/lib/generators/terrazzo/views/templates/fields/shared/TextInputFormField.jsx +2 -2
  38. data/lib/generators/terrazzo/views/templates/fields/text/FormField.jsx +2 -2
  39. data/lib/generators/terrazzo/views/templates/pages/_form.jsx +2 -2
  40. data/lib/generators/terrazzo/views/templates/pages/_navigation.json.props +5 -0
  41. data/lib/generators/terrazzo/views/templates/pages/edit.jsx +2 -3
  42. data/lib/generators/terrazzo/views/templates/pages/index.jsx +3 -14
  43. data/lib/generators/terrazzo/views/templates/pages/new.jsx +2 -3
  44. data/lib/generators/terrazzo/views/templates/pages/show.jsx +3 -4
  45. data/lib/generators/terrazzo/views/views_generator.rb +45 -26
  46. data/lib/terrazzo/base_dashboard.rb +4 -1
  47. data/lib/terrazzo/field/asset.rb +34 -0
  48. data/lib/terrazzo/field/associative.rb +1 -1
  49. data/lib/terrazzo/field/belongs_to.rb +2 -2
  50. data/lib/terrazzo/field/date.rb +2 -1
  51. data/lib/terrazzo/field/date_time.rb +2 -1
  52. data/lib/terrazzo/field/has_many.rb +8 -5
  53. data/lib/terrazzo/field/has_one.rb +2 -2
  54. data/lib/terrazzo/field/number.rb +4 -2
  55. data/lib/terrazzo/field/polymorphic.rb +3 -3
  56. data/lib/terrazzo/field/time.rb +2 -1
  57. data/lib/terrazzo/version.rb +1 -1
  58. data/lib/terrazzo.rb +1 -0
  59. data/terrazzo.gemspec +1 -1
  60. metadata +7 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8912409821769ce6f2303842822a9d33696bef4a24c9fdc9974411a99dccc7b0
4
- data.tar.gz: a171f982161da734439ac15c77cafa831ace5f8f9b8f87b43d93aa3107cf67d1
3
+ metadata.gz: f76fda78e25cb79116bd5f5f00b0b57e750503e3ebfdbee9a7bfe55dd8c17f34
4
+ data.tar.gz: 5cf113e4e1bfdd17591a3e50e1e993668da1408041efcc28a7f17b4a1b0eac05
5
5
  SHA512:
6
- metadata.gz: 1394dab4447f66fbda0801d3b60a74ddd8f0325b6927562c4f8cf7b33212491b029efc78b12d5d8eb887496fed9261b8abb3b75f4e5f2148d74a80061951643a
7
- data.tar.gz: f16c4e076d8f693b789622073df1fd9ccb5bedf36afb4eaa1032c4da09e8d87cc69b5a4929c8dcea28d0228c2ef53fc7476e71c8e12eee1e98a8b057982bad4a
6
+ metadata.gz: b60b58629158604c651402f6f920372f07146115b05f46613a2909be8af868f17005092bf7c84e288a4789724408e59150801fdc0b4f59c6575db1e05bb60f37
7
+ data.tar.gz: 9f6342b35c22e98a56f465221ef5e0bcc8911fcf75ee33ebea146144232e84a85b3c199b4ae14ce5969868c983587ca2cb59660ba96e2d2123c9ee1cc71aec36
@@ -16,7 +16,7 @@ module Terrazzo
16
16
 
17
17
  prepend Terrazzo::UsesSuperglue::TemplateLookupOverride
18
18
 
19
- helper_method :namespace, :dashboard, :resource_name, :resource_class, :application_title, :terrazzo_page_identifier
19
+ helper_method :namespace, :dashboard, :resource_name, :resource_class, :application_title, :terrazzo_page_identifier, :route_exists?
20
20
 
21
21
  def index
22
22
  search = Terrazzo::Search.new(scoped_resource, dashboard, params[:search])
@@ -196,9 +196,25 @@ module Terrazzo
196
196
  # Build a page identifier that matches the user's namespace, not the
197
197
  # engine's internal template path. The React page-to-component mapping
198
198
  # keys off this identifier (e.g. "admin/application/index").
199
+ #
200
+ # Map create → new and update → edit so that failed validations
201
+ # (which render :new / :edit) resolve to the correct React component.
202
+ TERRAZZO_ACTION_MAP = { "create" => "new", "update" => "edit" }.freeze
203
+
199
204
  def terrazzo_page_identifier
200
205
  ns = controller_path.split("/").first
201
- "#{ns}/application/#{action_name}"
206
+ mapped_action = TERRAZZO_ACTION_MAP[action_name] || action_name
207
+ "#{ns}/application/#{mapped_action}"
208
+ end
209
+
210
+ def route_exists?(action)
211
+ @_route_exists_cache ||= {}
212
+ return @_route_exists_cache[action] if @_route_exists_cache.key?(action)
213
+
214
+ @_route_exists_cache[action] = Rails.application.routes.routes.any? do |route|
215
+ route.defaults[:controller] == controller_path &&
216
+ route.defaults[:action] == action.to_s
217
+ end
202
218
  end
203
219
 
204
220
  private
@@ -0,0 +1,12 @@
1
+ resources = Terrazzo::Namespace.new(namespace).resources_with_index_route
2
+
3
+ json.array! [{ label: "Resources", resources: resources }] do |group|
4
+ json.label group[:label]
5
+ json.items do
6
+ json.array! group[:resources] do |r|
7
+ json.label r.resource_name.humanize.pluralize
8
+ json.path url_for(controller: "/#{r.controller_path}", action: :index, only_path: true)
9
+ json.active r.controller_path == controller_path
10
+ end
11
+ end
12
+ end
@@ -5,7 +5,8 @@ json.pageTitle t("terrazzo.actions.edit", resource_name: resource_name)
5
5
  json.form do
6
6
  json.props({
7
7
  action: polymorphic_path([namespace, @resource]),
8
- method: "post"
8
+ method: "post",
9
+ encType: "multipart/form-data"
9
10
  })
10
11
  json.extras do
11
12
  if protect_against_forgery?
@@ -60,11 +61,3 @@ rescue ActionController::UrlGenerationError
60
61
  nil
61
62
  end
62
63
  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
@@ -33,8 +33,8 @@ json.table do
33
33
  json.array! @resources do |resource|
34
34
  json.id resource.id
35
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
36
+ json.editPath route_exists?(:edit) ? (edit_polymorphic_path([namespace, resource]) rescue nil) : nil
37
+ json.deletePath route_exists?(:destroy) ? (polymorphic_path([namespace, resource]) rescue nil) : nil
38
38
 
39
39
  json.cells do
40
40
  json.array! @page.attribute_names do |attr|
@@ -89,12 +89,4 @@ end
89
89
 
90
90
  json.resourceName resource_name.pluralize
91
91
  json.singularResourceName resource_name
92
- json.newResourcePath new_polymorphic_path([namespace, resource_class]) rescue nil
93
-
94
- json.navigation do
95
- json.array! Terrazzo::Namespace.new(namespace).resources_with_index_route do |nav_resource|
96
- json.label nav_resource.resource_name.humanize.pluralize
97
- json.path url_for(controller: "/#{nav_resource.controller_path}", action: :index, only_path: true)
98
- json.active nav_resource.controller_path == controller_path
99
- end
100
- end
92
+ json.newResourcePath route_exists?(:new) ? (new_polymorphic_path([namespace, resource_class]) rescue nil) : nil
@@ -6,7 +6,8 @@ json.pageTitle t("terrazzo.actions.new", resource_name: resource_name)
6
6
  json.form do
7
7
  json.props({
8
8
  action: polymorphic_path([namespace, resource_class]),
9
- method: "post"
9
+ method: "post",
10
+ encType: "multipart/form-data"
10
11
  })
11
12
  json.extras do
12
13
  if protect_against_forgery?
@@ -55,11 +56,3 @@ rescue ActionController::UrlGenerationError
55
56
  nil
56
57
  end
57
58
  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
@@ -34,8 +34,8 @@ json.attributes do
34
34
  end
35
35
  end
36
36
 
37
- json.editPath edit_polymorphic_path([namespace, @resource]) rescue nil
38
- json.deletePath polymorphic_path([namespace, @resource]) rescue nil
37
+ json.editPath route_exists?(:edit) ? (edit_polymorphic_path([namespace, @resource]) rescue nil) : nil
38
+ json.deletePath route_exists?(:destroy) ? (polymorphic_path([namespace, @resource]) rescue nil) : nil
39
39
  json.indexPath begin
40
40
  url_for(controller: controller_path, action: :index, only_path: true)
41
41
  rescue ActionController::UrlGenerationError
@@ -43,11 +43,3 @@ rescue ActionController::UrlGenerationError
43
43
  end
44
44
  json.resourceName resource_name
45
45
  json.pluralResourceName resource_name.pluralize
46
-
47
- json.navigation do
48
- json.array! Terrazzo::Namespace.new(namespace).resources_with_index_route do |nav_resource|
49
- json.label nav_resource.resource_name.humanize.pluralize
50
- json.path url_for(controller: "/#{nav_resource.controller_path}", action: :index, only_path: true)
51
- json.active nav_resource.controller_path == controller_path
52
- end
53
- end
@@ -71,8 +71,18 @@ module Terrazzo
71
71
  types[col.name.to_sym] = column_to_field_type(col)
72
72
  end
73
73
 
74
+ # Active Storage attachment names (used to filter internal associations)
75
+ attachment_names = if model_class.respond_to?(:reflect_on_all_attachments)
76
+ model_class.reflect_on_all_attachments.map(&:name).to_set
77
+ else
78
+ Set.new
79
+ end
80
+
74
81
  # Associations
75
82
  associations.each do |assoc|
83
+ # Skip Active Storage internal associations (e.g., document_attachment, document_blob)
84
+ next if active_storage_internal?(assoc.name, attachment_names)
85
+
76
86
  case assoc.macro
77
87
  when :belongs_to
78
88
  types[assoc.name] = assoc.options[:polymorphic] ? "Field::Polymorphic" : "Field::BelongsTo"
@@ -83,6 +93,13 @@ module Terrazzo
83
93
  end
84
94
  end
85
95
 
96
+ # Active Storage attachments
97
+ attachment_names.each do |name|
98
+ attachment = model_class.reflect_on_attachment(name)
99
+ next if attachment.macro == :has_many_attached
100
+ types[name] = "Field::Asset"
101
+ end
102
+
86
103
  types
87
104
  end
88
105
 
@@ -106,6 +123,15 @@ module Terrazzo
106
123
  end
107
124
  end
108
125
 
126
+ def active_storage_internal?(assoc_name, attachment_names)
127
+ name = assoc_name.to_s
128
+ attachment_names.any? do |att|
129
+ att_s = att.to_s
130
+ name == "#{att_s}_attachment" || name == "#{att_s}_blob" ||
131
+ name == "#{att_s}_attachments" || name == "#{att_s}_blobs"
132
+ end
133
+ end
134
+
109
135
  def has_enum?(column_name)
110
136
  model_class.defined_enums.key?(column_name.to_s)
111
137
  end
@@ -0,0 +1,232 @@
1
+ require "rails/generators"
2
+
3
+ module Terrazzo
4
+ module Generators
5
+ class EjectGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("../views/templates", __dir__)
7
+
8
+ argument :target, type: :string,
9
+ desc: "What to eject (e.g., fields/string, components/Layout, ui/button, pages/index)"
10
+
11
+ class_option :namespace, type: :string, default: "admin",
12
+ desc: "Admin namespace"
13
+
14
+ def eject
15
+ case category
16
+ when "fields"
17
+ eject_field
18
+ when "components"
19
+ eject_component
20
+ when "ui"
21
+ eject_ui
22
+ when "pages"
23
+ eject_page
24
+ when "navigation"
25
+ eject_navigation
26
+ else
27
+ say_status :error, "Unknown category '#{category}'. Use fields/, components/, ui/, pages/, or navigation", :red
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def category
34
+ target.split("/").first
35
+ end
36
+
37
+ def component_name
38
+ target.split("/", 2).last
39
+ end
40
+
41
+ def namespace_name
42
+ options[:namespace]
43
+ end
44
+
45
+ def eject_field
46
+ field_type = component_name
47
+ source_dir = "fields/#{field_type}"
48
+
49
+ unless File.directory?(File.join(self.class.source_root, source_dir))
50
+ say_status :error, "Unknown field type '#{field_type}'", :red
51
+ return
52
+ end
53
+
54
+ %w[IndexField.jsx ShowField.jsx FormField.jsx].each do |file|
55
+ source = File.join(source_dir, file)
56
+ next unless File.exist?(File.join(self.class.source_root, source))
57
+ copy_file source, "app/views/#{namespace_name}/fields/#{field_type}/#{file}"
58
+ end
59
+
60
+ # Copy shared dependencies if needed
61
+ if field_uses_shared?(field_type)
62
+ copy_file "fields/shared/TextInputFormField.jsx",
63
+ "app/views/#{namespace_name}/fields/shared/TextInputFormField.jsx"
64
+ end
65
+
66
+ update_fields_barrel(field_type)
67
+ end
68
+
69
+ def eject_component
70
+ name = component_name
71
+ source = "components/#{name}.jsx"
72
+
73
+ unless File.exist?(File.join(self.class.source_root, source))
74
+ say_status :error, "Unknown component '#{name}'", :red
75
+ return
76
+ end
77
+
78
+ copy_file source, "app/views/#{namespace_name}/components/#{name}.jsx"
79
+ update_components_barrel(name)
80
+ end
81
+
82
+ def eject_ui
83
+ name = component_name
84
+ source = "components/ui/#{name}.jsx"
85
+
86
+ unless File.exist?(File.join(self.class.source_root, source))
87
+ say_status :error, "Unknown UI component '#{name}'", :red
88
+ return
89
+ end
90
+
91
+ copy_file source, "app/views/#{namespace_name}/components/ui/#{name}.jsx"
92
+ update_ui_barrel(name)
93
+ end
94
+
95
+ def eject_page
96
+ name = component_name
97
+ source = "pages/#{name}.jsx"
98
+
99
+ unless File.exist?(File.join(self.class.source_root, source))
100
+ say_status :error, "Unknown page template '#{name}'", :red
101
+ return
102
+ end
103
+
104
+ copy_file source, "app/views/#{namespace_name}/application/#{name}.jsx"
105
+ end
106
+
107
+ def eject_navigation
108
+ source = File.join(Terrazzo::Engine.root, "app/views/terrazzo/application/_navigation.json.props")
109
+ dest = "app/views/#{namespace_name}/application/_navigation.json.props"
110
+ copy_file source, dest
111
+ say "\nNavigation partial ejected to #{dest}.", :green
112
+ say "Edit it to customize your admin navigation."
113
+ end
114
+
115
+ def field_uses_shared?(field_type)
116
+ %w[string number email url password date date_time time].include?(field_type)
117
+ end
118
+
119
+ def update_fields_barrel(field_type)
120
+ barrel_path = "app/views/#{namespace_name}/fields/index.js"
121
+ barrel_file = File.join(destination_root, barrel_path)
122
+
123
+ type_label = field_type.split("_").map(&:capitalize).join("")
124
+ local_exports = <<~JS.strip
125
+ // #{type_label} - ejected
126
+ export { IndexField as #{type_label}IndexField } from "./#{field_type}/IndexField";
127
+ export { ShowField as #{type_label}ShowField } from "./#{field_type}/ShowField";
128
+ export { FormField as #{type_label}FormField } from "./#{field_type}/FormField";
129
+ JS
130
+
131
+ if File.exist?(barrel_file)
132
+ content = File.read(barrel_file)
133
+
134
+ # If barrel is still the default re-export-all, replace with explicit exports
135
+ if content.include?('export * from "terrazzo/fields"')
136
+ new_content = build_fields_barrel_with_ejection(field_type)
137
+ create_file barrel_path, new_content, force: true
138
+ else
139
+ # Barrel already has explicit exports; replace the line for this field type
140
+ # by inserting local exports before the terrazzo re-exports
141
+ unless content.include?("./#{field_type}/")
142
+ append_to_file barrel_path, "\n#{local_exports}\n"
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ def build_fields_barrel_with_ejection(ejected_field_type)
149
+ all_field_types = %w[
150
+ string text number boolean date date_time time email url password
151
+ select rich_text belongs_to has_many has_one polymorphic hstore
152
+ ]
153
+
154
+ lines = ['export { FieldRenderer, registerFieldType } from "terrazzo/fields";', ""]
155
+
156
+ all_field_types.each do |ft|
157
+ label = ft.split("_").map(&:capitalize).join("")
158
+ if ft == ejected_field_type
159
+ lines << "// #{label} - ejected"
160
+ lines << "export { IndexField as #{label}IndexField } from \"./#{ft}/IndexField\";"
161
+ lines << "export { ShowField as #{label}ShowField } from \"./#{ft}/ShowField\";"
162
+ lines << "export { FormField as #{label}FormField } from \"./#{ft}/FormField\";"
163
+ else
164
+ lines << "export { #{label}IndexField, #{label}ShowField, #{label}FormField } from \"terrazzo/fields\";"
165
+ end
166
+ end
167
+
168
+ lines.join("\n") + "\n"
169
+ end
170
+
171
+ def update_components_barrel(name)
172
+ barrel_path = "app/views/#{namespace_name}/components/index.js"
173
+ barrel_file = File.join(destination_root, barrel_path)
174
+
175
+ return unless File.exist?(barrel_file)
176
+
177
+ content = File.read(barrel_file)
178
+ if content.include?('export * from "terrazzo/components"')
179
+ export_name = component_export_name(name)
180
+ new_content = build_components_barrel_with_ejection(name, export_name)
181
+ create_file barrel_path, new_content, force: true
182
+ end
183
+ end
184
+
185
+ def build_components_barrel_with_ejection(ejected_name, export_name)
186
+ all_components = {
187
+ "Layout" => "Layout",
188
+ "app-sidebar" => "AppSidebar",
189
+ "site-header" => "SiteHeader",
190
+ "FlashMessages" => "FlashMessages",
191
+ "SearchBar" => "SearchBar",
192
+ "Pagination" => "Pagination",
193
+ "SortableHeader" => "SortableHeader",
194
+ }
195
+
196
+ lines = []
197
+ all_components.each do |file_name, export|
198
+ if file_name == ejected_name
199
+ lines << "export { #{export} } from \"./#{file_name}\"; // ejected"
200
+ else
201
+ lines << "export { #{export} } from \"terrazzo/components\";"
202
+ end
203
+ end
204
+
205
+ lines.join("\n") + "\n"
206
+ end
207
+
208
+ def update_ui_barrel(name)
209
+ barrel_path = "app/views/#{namespace_name}/components/ui/index.js"
210
+ barrel_file = File.join(destination_root, barrel_path)
211
+
212
+ return unless File.exist?(barrel_file)
213
+
214
+ content = File.read(barrel_file)
215
+ if content.include?('export * from "terrazzo/ui"')
216
+ new_content = "export * from \"terrazzo/ui\";\n"
217
+ new_content += "// Override ejected UI component:\n"
218
+ new_content += "export * from \"./#{name}\";\n"
219
+ create_file barrel_path, new_content, force: true
220
+ end
221
+ end
222
+
223
+ def component_export_name(file_name)
224
+ case file_name
225
+ when "app-sidebar" then "AppSidebar"
226
+ when "site-header" then "SiteHeader"
227
+ else file_name
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
@@ -20,15 +20,22 @@ module Terrazzo
20
20
  end
21
21
  end
22
22
 
23
- def show_registration_instructions
24
- say "\nTo use your custom field, register it in your FieldRenderer.jsx:", :green
25
- say " import { IndexField as #{class_name}Index } from \"./#{file_name}/IndexField\";"
26
- say " import { ShowField as #{class_name}Show } from \"./#{file_name}/ShowField\";"
27
- say " import { FormField as #{class_name}Form } from \"./#{file_name}/FormField\";"
28
- say ""
29
- say " registerFieldType(\"#{file_name}\", { index: #{class_name}Index, show: #{class_name}Show, form: #{class_name}Form });"
30
- say ""
31
- say "Then use it in your dashboard:"
23
+ def register_in_barrel
24
+ barrel_path = File.join(destination_root, "app/views/#{namespace_name}/fields/index.js")
25
+ return unless File.exist?(barrel_path)
26
+
27
+ registration = <<~JS
28
+
29
+ // #{class_name} - custom field
30
+ export { IndexField as #{class_name}IndexField } from "./#{file_name}/IndexField";
31
+ export { ShowField as #{class_name}ShowField } from "./#{file_name}/ShowField";
32
+ export { FormField as #{class_name}FormField } from "./#{file_name}/FormField";
33
+ JS
34
+
35
+ append_to_file "app/views/#{namespace_name}/fields/index.js", registration
36
+
37
+ say "\nCustom field '#{file_name}' registered in fields/index.js.", :green
38
+ say "Use it in your dashboard:"
32
39
  say " #{file_name}: Terrazzo::Field::#{class_name},"
33
40
  end
34
41
 
@@ -1,6 +1,5 @@
1
1
  import React from "react";
2
- import { Input } from "../../components/ui/input";
3
- import { Label } from "../../components/ui/label";
2
+ import { Input, Label } from "terrazzo/ui";
4
3
 
5
4
  export function FormField({ attribute, label, value, input, required }) {
6
5
  return (
@@ -21,7 +21,7 @@ module Terrazzo
21
21
  end
22
22
 
23
23
  def create_json_props_layout
24
- copy_file "application.json.props",
24
+ template "application.json.props.tt",
25
25
  "app/views/layouts/#{namespace_name}/application.json.props"
26
26
  end
27
27
 
@@ -1,4 +1,5 @@
1
1
  @import "tailwindcss";
2
+ @source "../../../node_modules/terrazzo/dist";
2
3
 
3
4
  @custom-variant dark (&:is(.dark *));
4
5
 
@@ -1,6 +1,7 @@
1
1
  path = request.format.json? ? param_to_dig_path(params[:props_at]) : nil
2
2
 
3
3
  json.data(dig: path) do
4
+ json.navigation(partial: ["<%= namespace_name %>/application/navigation"]) {}
4
5
  yield
5
6
  end
6
7
 
@@ -1,17 +1,7 @@
1
- <% if vite? -%>
2
- const pages = import.meta.glob("../../views/<%= namespace_name %>/**/*.jsx", { eager: true })
3
-
4
- export const pageToPageMapping = {}
5
-
6
- for (const key in pages) {
7
- const identifier = key.replace("../../views/", "").replace(/\.jsx$/, "")
8
- pageToPageMapping[identifier] = pages[key].default
9
- }
10
- <% else -%>
11
- import AdminIndex from "../../views/<%= namespace_name %>/application/index"
12
- import AdminShow from "../../views/<%= namespace_name %>/application/show"
13
- import AdminNew from "../../views/<%= namespace_name %>/application/new"
14
- import AdminEdit from "../../views/<%= namespace_name %>/application/edit"
1
+ import AdminIndex from "../../views/<%= namespace_name %>/application/index";
2
+ import AdminShow from "../../views/<%= namespace_name %>/application/show";
3
+ import AdminNew from "../../views/<%= namespace_name %>/application/new";
4
+ import AdminEdit from "../../views/<%= namespace_name %>/application/edit";
15
5
 
16
6
  export const pageToPageMapping = {
17
7
  '<%= namespace_name %>/application/index': AdminIndex,
@@ -19,4 +9,3 @@ export const pageToPageMapping = {
19
9
  '<%= namespace_name %>/application/new': AdminNew,
20
10
  '<%= namespace_name %>/application/edit': AdminEdit,
21
11
  }
22
- <% end -%>
@@ -3,7 +3,7 @@ import React from "react";
3
3
  import { AppSidebar } from "./app-sidebar";
4
4
  import { SiteHeader } from "./site-header";
5
5
  import { FlashMessages } from "./FlashMessages";
6
- import { SidebarProvider, SidebarInset } from "./ui/sidebar";
6
+ import { SidebarProvider, SidebarInset } from "terrazzo/ui";
7
7
 
8
8
  export function Layout({ navigation, title, actions, children }) {
9
9
  return (
@@ -1,14 +1,14 @@
1
1
  import React, { useContext } from "react";
2
2
  import { NavigationContext } from "@thoughtbot/superglue";
3
3
 
4
- import { Field, FieldLabel } from "./ui/field";
4
+ import { Field, FieldLabel } from "terrazzo/ui";
5
5
  import {
6
6
  Pagination as PaginationRoot,
7
7
  PaginationContent,
8
8
  PaginationItem,
9
9
  PaginationNext,
10
10
  PaginationPrevious,
11
- } from "./ui/pagination";
11
+ } from "terrazzo/ui";
12
12
  import {
13
13
  Select,
14
14
  SelectContent,
@@ -16,7 +16,7 @@ import {
16
16
  SelectItem,
17
17
  SelectTrigger,
18
18
  SelectValue,
19
- } from "./ui/select";
19
+ } from "terrazzo/ui";
20
20
 
21
21
  export function Pagination({ currentPage, totalPages, totalCount, perPage, nextPagePath, prevPagePath }) {
22
22
  const { remote, pageKey } = useContext(NavigationContext);
@@ -1,7 +1,7 @@
1
1
  import React, { useRef, useContext } from "react";
2
2
  import { NavigationContext } from "@thoughtbot/superglue";
3
3
 
4
- import { Input } from "./ui/input";
4
+ import { Input } from "terrazzo/ui";
5
5
  import { Search } from "lucide-react";
6
6
 
7
7
  export function SearchBar({ searchTerm, searchPath }) {
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react";
3
3
 
4
- import { TableHead } from "./ui/table";
4
+ import { TableHead } from "terrazzo/ui";
5
5
 
6
6
  export function SortableHeader({ label, sortable, sortUrl, sortDirection }) {
7
7
  if (!sortable) {
@@ -11,7 +11,7 @@ import {
11
11
  SidebarMenu,
12
12
  SidebarMenuButton,
13
13
  SidebarMenuItem,
14
- } from "./ui/sidebar";
14
+ } from "terrazzo/ui";
15
15
 
16
16
  export function AppSidebar({
17
17
  navigation,
@@ -27,7 +27,7 @@ export function AppSidebar({
27
27
  asChild
28
28
  className="data-[slot=sidebar-menu-button]:!p-1.5">
29
29
 
30
- <a href={navigation[0]?.path ?? "#"} data-sg-visit>
30
+ <a href={navigation[0]?.items?.[0]?.path ?? navigation[0]?.path ?? "#"} data-sg-visit>
31
31
  <LayoutDashboardIcon className="h-5 w-5" />
32
32
  <span className="text-base font-semibold">{title}</span>
33
33
  </a>
@@ -36,26 +36,27 @@ export function AppSidebar({
36
36
  </SidebarMenu>
37
37
  </SidebarHeader>
38
38
  <SidebarContent>
39
- <SidebarGroup>
40
- <SidebarGroupLabel>Resources</SidebarGroupLabel>
41
- <SidebarGroupContent>
42
- <SidebarMenu>
43
- {navigation.map((item) =>
44
- <SidebarMenuItem key={item.path}>
45
- <SidebarMenuButton
46
- asChild
47
- isActive={item.active}
48
- tooltip={item.label}>
49
-
50
- <a href={item.path} data-sg-visit>
51
- <span>{item.label}</span>
52
- </a>
53
- </SidebarMenuButton>
54
- </SidebarMenuItem>
55
- )}
56
- </SidebarMenu>
57
- </SidebarGroupContent>
58
- </SidebarGroup>
39
+ {navigation.map((group) =>
40
+ <SidebarGroup key={group.label}>
41
+ <SidebarGroupLabel>{group.label}</SidebarGroupLabel>
42
+ <SidebarGroupContent>
43
+ <SidebarMenu>
44
+ {group.items.map((item) =>
45
+ <SidebarMenuItem key={item.path}>
46
+ <SidebarMenuButton
47
+ asChild
48
+ isActive={item.active}
49
+ tooltip={item.label}>
50
+ <a href={item.path} data-sg-visit>
51
+ <span>{item.label}</span>
52
+ </a>
53
+ </SidebarMenuButton>
54
+ </SidebarMenuItem>
55
+ )}
56
+ </SidebarMenu>
57
+ </SidebarGroupContent>
58
+ </SidebarGroup>
59
+ )}
59
60
  </SidebarContent>
60
61
  </Sidebar>);
61
62