terrazzo 0.3.1 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f76fda78e25cb79116bd5f5f00b0b57e750503e3ebfdbee9a7bfe55dd8c17f34
4
- data.tar.gz: 5cf113e4e1bfdd17591a3e50e1e993668da1408041efcc28a7f17b4a1b0eac05
3
+ metadata.gz: 6da1376c6052a70bacdbb878ee703543e04e0dc044f57cb5b04c35aa08ea536c
4
+ data.tar.gz: 9fe6747b3ee47446e501a37c82978a1e1d00fc33b517ad8c8a34ec13b9620889
5
5
  SHA512:
6
- metadata.gz: b60b58629158604c651402f6f920372f07146115b05f46613a2909be8af868f17005092bf7c84e288a4789724408e59150801fdc0b4f59c6575db1e05bb60f37
7
- data.tar.gz: 9f6342b35c22e98a56f465221ef5e0bcc8911fcf75ee33ebea146144232e84a85b3c199b4ae14ce5969868c983587ca2cb59660ba96e2d2123c9ee1cc71aec36
6
+ metadata.gz: 316fb61ca947bd8a4d581bcd46a6b1c74a4b0b6a2b6c8113e27ceed6df52f124721a2c8b12d09fb7fbe9f09eb69e8a4ead22d79175946d9d1f74a33da23612a9
7
+ data.tar.gz: d15cdb461bfc96aa705c463f0f174ce8490a836afaf8755225d6268edd90462e94019687f3f717a4559e32de3a4dc6711d3b580546477b8f7e674e164f866710
@@ -17,6 +17,7 @@ module Terrazzo
17
17
  prepend Terrazzo::UsesSuperglue::TemplateLookupOverride
18
18
 
19
19
  helper_method :namespace, :dashboard, :resource_name, :resource_class, :application_title, :terrazzo_page_identifier, :route_exists?
20
+ helper Terrazzo::CollectionActionsHelper
20
21
 
21
22
  def index
22
23
  search = Terrazzo::Search.new(scoped_resource, dashboard, params[:search])
@@ -129,9 +130,6 @@ module Terrazzo
129
130
 
130
131
  def find_resource(id)
131
132
  scoped_resource.find(id)
132
- rescue ActiveRecord::RecordNotFound
133
- # Support models that override to_param (e.g., slug-based URLs)
134
- scoped_resource.find_by!(slug: id)
135
133
  end
136
134
 
137
135
  def resource_params(action = nil)
@@ -0,0 +1,22 @@
1
+ module Terrazzo
2
+ module CollectionActionsHelper
3
+ def collection_item_actions(resource)
4
+ resource_dashboard = "#{resource.class.name}Dashboard".safe_constantize&.new
5
+ if resource_dashboard&.respond_to?(:collection_item_actions)
6
+ resource_dashboard.collection_item_actions(resource, self)
7
+ else
8
+ default_collection_item_actions(resource)
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def default_collection_item_actions(resource)
15
+ actions = []
16
+ actions << { label: "Show", url: polymorphic_path([namespace, resource]) } rescue nil
17
+ actions << { label: "Edit", url: edit_polymorphic_path([namespace, resource]) } rescue nil
18
+ actions << { label: "Destroy", url: polymorphic_path([namespace, resource]), method: "delete", confirm: "Are you sure?" } rescue nil
19
+ actions.compact
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,63 @@
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
+ encType: "multipart/form-data"
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
+ json.set!("_method") do
21
+ json.name "_method"
22
+ json.type "hidden"
23
+ json.value "patch"
24
+ end
25
+ end
26
+ json.fields do
27
+ json.array! @page.attributes("update") do |field|
28
+ json.attribute field.attribute.to_s
29
+ json.label field.attribute.to_s.humanize
30
+ json.fieldType field.field_type
31
+ json.value field.serialize_value(:form)
32
+ json.options field.serializable_options
33
+ json.required field.required?
34
+ json.input field.form_input_attributes(param_key)
35
+ end
36
+ end
37
+ json.fieldGroups do
38
+ json.array! @page.grouped_attributes("update") do |group|
39
+ json.name group[:name]
40
+ json.fields do
41
+ json.array! group[:fields] do |field|
42
+ json.attribute field.attribute.to_s
43
+ json.label field.attribute.to_s.humanize
44
+ json.fieldType field.field_type
45
+ json.value field.serialize_value(:form)
46
+ json.options field.serializable_options
47
+ json.required field.required?
48
+ json.input field.form_input_attributes(param_key)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ json.errors @resource.errors.full_messages
56
+
57
+ json.showPath polymorphic_path([namespace, @resource])
58
+ json.indexPath begin
59
+ url_for(controller: controller_path, action: :index, only_path: true)
60
+ rescue ActionController::UrlGenerationError
61
+ nil
62
+ end
63
+ json.resourceName resource_name
@@ -0,0 +1,91 @@
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.collectionItemActions collection_item_actions(resource)
37
+
38
+ json.cells do
39
+ json.array! @page.attribute_names do |attr|
40
+ field = dashboard.attribute_type_for(attr).new(attr, nil, :index, resource: resource)
41
+ json.attribute attr.to_s
42
+ json.fieldType field.field_type
43
+ json.value field.serialize_value(:index)
44
+
45
+ if field.class.associative? && field.data.present?
46
+ json.showPath polymorphic_path([namespace, field.data]) rescue nil
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ json.pagination do
55
+ json.currentPage @resources.current_page
56
+ json.totalPages @resources.total_pages
57
+ json.totalCount @resources.total_count
58
+ json.perPage @resources.limit_value
59
+ if @resources.next_page
60
+ json.nextPagePath url_for(
61
+ _page: @resources.next_page,
62
+ search: @search_term,
63
+ order: params[:order],
64
+ direction: params[:direction],
65
+ filter: @active_filter,
66
+ filter_value: @filter_value,
67
+ props_at: "data.pagination",
68
+ only_path: true
69
+ )
70
+ else
71
+ json.nextPagePath nil
72
+ end
73
+ if @resources.prev_page
74
+ json.prevPagePath url_for(
75
+ _page: @resources.prev_page,
76
+ search: @search_term,
77
+ order: params[:order],
78
+ direction: params[:direction],
79
+ filter: @active_filter,
80
+ filter_value: @filter_value,
81
+ props_at: "data.pagination",
82
+ only_path: true
83
+ )
84
+ else
85
+ json.prevPagePath nil
86
+ end
87
+ end
88
+
89
+ json.resourceName resource_name.pluralize
90
+ json.singularResourceName resource_name
91
+ json.newResourcePath route_exists?(:new) ? (new_polymorphic_path([namespace, resource_class]) rescue nil) : nil
@@ -0,0 +1,58 @@
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
+ encType: "multipart/form-data"
11
+ })
12
+ json.extras do
13
+ if protect_against_forgery?
14
+ json.set!("authenticity_token") do
15
+ json.name "authenticity_token"
16
+ json.type "hidden"
17
+ json.defaultValue form_authenticity_token
18
+ json.autoComplete "off"
19
+ end
20
+ end
21
+ end
22
+ json.fields do
23
+ json.array! @page.attributes(action) do |field|
24
+ json.attribute field.attribute.to_s
25
+ json.label field.attribute.to_s.humanize
26
+ json.fieldType field.field_type
27
+ json.value field.serialize_value(:form)
28
+ json.options field.serializable_options
29
+ json.required field.required?
30
+ json.input field.form_input_attributes(param_key)
31
+ end
32
+ end
33
+ json.fieldGroups do
34
+ json.array! @page.grouped_attributes(action) do |group|
35
+ json.name group[:name]
36
+ json.fields do
37
+ json.array! group[:fields] do |field|
38
+ json.attribute field.attribute.to_s
39
+ json.label field.attribute.to_s.humanize
40
+ json.fieldType field.field_type
41
+ json.value field.serialize_value(:form)
42
+ json.options field.serializable_options
43
+ json.required field.required?
44
+ json.input field.form_input_attributes(param_key)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ json.errors @resource.errors.full_messages
52
+
53
+ json.indexPath begin
54
+ url_for(controller: controller_path, action: :index, only_path: true)
55
+ rescue ActionController::UrlGenerationError
56
+ nil
57
+ end
58
+ json.resourceName resource_name
@@ -0,0 +1,48 @@
1
+ json.pageTitle @page.page_title
2
+
3
+ show_field_json = ->(json, field) do
4
+ json.attribute field.attribute.to_s
5
+ json.label field.attribute.to_s.humanize
6
+ json.fieldType field.field_type
7
+ json.value field.serialize_value(:show)
8
+
9
+ if field.class.associative? && field.data.present?
10
+ if field.is_a?(Terrazzo::Field::HasMany)
11
+ json.itemShowPaths(field.data.each_with_object({}) do |record, paths|
12
+ paths[record.id.to_s] = polymorphic_path([namespace, record]) rescue nil
13
+ end)
14
+ json.collectionItemActions(field.data.each_with_object({}) do |record, hash|
15
+ hash[record.id.to_s] = collection_item_actions(record)
16
+ end)
17
+ else
18
+ json.showPath polymorphic_path([namespace, field.data]) rescue nil
19
+ end
20
+ end
21
+ end
22
+
23
+ json.attributeGroups do
24
+ json.array! @page.grouped_attributes do |group|
25
+ json.name group[:name]
26
+ json.attributes do
27
+ json.array! group[:fields] do |field|
28
+ show_field_json.call(json, field)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ json.attributes do
35
+ json.array! @page.attributes do |field|
36
+ show_field_json.call(json, field)
37
+ end
38
+ end
39
+
40
+ json.editPath route_exists?(:edit) ? (edit_polymorphic_path([namespace, @resource]) rescue nil) : nil
41
+ json.deletePath route_exists?(:destroy) ? (polymorphic_path([namespace, @resource]) rescue nil) : nil
42
+ json.indexPath begin
43
+ url_for(controller: controller_path, action: :index, only_path: true)
44
+ rescue ActionController::UrlGenerationError
45
+ nil
46
+ end
47
+ json.resourceName resource_name
48
+ json.pluralResourceName resource_name.pluralize
@@ -1,63 +1 @@
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
- encType: "multipart/form-data"
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
- json.set!("_method") do
21
- json.name "_method"
22
- json.type "hidden"
23
- json.value "patch"
24
- end
25
- end
26
- json.fields do
27
- json.array! @page.attributes("update") do |field|
28
- json.attribute field.attribute.to_s
29
- json.label field.attribute.to_s.humanize
30
- json.fieldType field.field_type
31
- json.value field.serialize_value(:form)
32
- json.options field.serializable_options
33
- json.required field.required?
34
- json.input field.form_input_attributes(param_key)
35
- end
36
- end
37
- json.fieldGroups do
38
- json.array! @page.grouped_attributes("update") do |group|
39
- json.name group[:name]
40
- json.fields do
41
- json.array! group[:fields] do |field|
42
- json.attribute field.attribute.to_s
43
- json.label field.attribute.to_s.humanize
44
- json.fieldType field.field_type
45
- json.value field.serialize_value(:form)
46
- json.options field.serializable_options
47
- json.required field.required?
48
- json.input field.form_input_attributes(param_key)
49
- end
50
- end
51
- end
52
- end
53
- end
54
-
55
- json.errors @resource.errors.full_messages
56
-
57
- json.showPath polymorphic_path([namespace, @resource])
58
- json.indexPath begin
59
- url_for(controller: controller_path, action: :index, only_path: true)
60
- rescue ActionController::UrlGenerationError
61
- nil
62
- end
63
- json.resourceName resource_name
1
+ json.partial! partial: "terrazzo/application/edit_base"
@@ -1,92 +1 @@
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 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
-
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
-
46
- if field.class.associative? && field.data.present?
47
- json.showPath polymorphic_path([namespace, field.data]) rescue nil
48
- end
49
- end
50
- end
51
- end
52
- end
53
- end
54
-
55
- json.pagination do
56
- json.currentPage @resources.current_page
57
- json.totalPages @resources.total_pages
58
- json.totalCount @resources.total_count
59
- json.perPage @resources.limit_value
60
- if @resources.next_page
61
- json.nextPagePath url_for(
62
- _page: @resources.next_page,
63
- search: @search_term,
64
- order: params[:order],
65
- direction: params[:direction],
66
- filter: @active_filter,
67
- filter_value: @filter_value,
68
- props_at: "data.pagination",
69
- only_path: true
70
- )
71
- else
72
- json.nextPagePath nil
73
- end
74
- if @resources.prev_page
75
- json.prevPagePath url_for(
76
- _page: @resources.prev_page,
77
- search: @search_term,
78
- order: params[:order],
79
- direction: params[:direction],
80
- filter: @active_filter,
81
- filter_value: @filter_value,
82
- props_at: "data.pagination",
83
- only_path: true
84
- )
85
- else
86
- json.prevPagePath nil
87
- end
88
- end
89
-
90
- json.resourceName resource_name.pluralize
91
- json.singularResourceName resource_name
92
- json.newResourcePath route_exists?(:new) ? (new_polymorphic_path([namespace, resource_class]) rescue nil) : nil
1
+ json.partial! partial: "terrazzo/application/index_base"
@@ -1,58 +1 @@
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
- encType: "multipart/form-data"
11
- })
12
- json.extras do
13
- if protect_against_forgery?
14
- json.set!("authenticity_token") do
15
- json.name "authenticity_token"
16
- json.type "hidden"
17
- json.defaultValue form_authenticity_token
18
- json.autoComplete "off"
19
- end
20
- end
21
- end
22
- json.fields do
23
- json.array! @page.attributes(action) do |field|
24
- json.attribute field.attribute.to_s
25
- json.label field.attribute.to_s.humanize
26
- json.fieldType field.field_type
27
- json.value field.serialize_value(:form)
28
- json.options field.serializable_options
29
- json.required field.required?
30
- json.input field.form_input_attributes(param_key)
31
- end
32
- end
33
- json.fieldGroups do
34
- json.array! @page.grouped_attributes(action) do |group|
35
- json.name group[:name]
36
- json.fields do
37
- json.array! group[:fields] do |field|
38
- json.attribute field.attribute.to_s
39
- json.label field.attribute.to_s.humanize
40
- json.fieldType field.field_type
41
- json.value field.serialize_value(:form)
42
- json.options field.serializable_options
43
- json.required field.required?
44
- json.input field.form_input_attributes(param_key)
45
- end
46
- end
47
- end
48
- end
49
- end
50
-
51
- json.errors @resource.errors.full_messages
52
-
53
- json.indexPath begin
54
- url_for(controller: controller_path, action: :index, only_path: true)
55
- rescue ActionController::UrlGenerationError
56
- nil
57
- end
58
- json.resourceName resource_name
1
+ json.partial! partial: "terrazzo/application/new_base"
@@ -1,45 +1 @@
1
- json.pageTitle @page.page_title
2
-
3
- show_field_json = ->(json, field) do
4
- json.attribute field.attribute.to_s
5
- json.label field.attribute.to_s.humanize
6
- json.fieldType field.field_type
7
- json.value field.serialize_value(:show)
8
-
9
- if field.class.associative? && field.data.present?
10
- if field.is_a?(Terrazzo::Field::HasMany)
11
- json.itemShowPaths(field.data.each_with_object({}) do |record, paths|
12
- paths[record.id.to_s] = polymorphic_path([namespace, record]) rescue nil
13
- end)
14
- else
15
- json.showPath polymorphic_path([namespace, field.data]) rescue nil
16
- end
17
- end
18
- end
19
-
20
- json.attributeGroups do
21
- json.array! @page.grouped_attributes do |group|
22
- json.name group[:name]
23
- json.attributes do
24
- json.array! group[:fields] do |field|
25
- show_field_json.call(json, field)
26
- end
27
- end
28
- end
29
- end
30
-
31
- json.attributes do
32
- json.array! @page.attributes do |field|
33
- show_field_json.call(json, field)
34
- end
35
- end
36
-
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
- json.indexPath begin
40
- url_for(controller: controller_path, action: :index, only_path: true)
41
- rescue ActionController::UrlGenerationError
42
- nil
43
- end
44
- json.resourceName resource_name
45
- json.pluralResourceName resource_name.pluralize
1
+ json.partial! partial: "terrazzo/application/show_base"
@@ -6,18 +6,66 @@ module Terrazzo
6
6
  class EditGenerator < Rails::Generators::Base
7
7
  source_root File.expand_path("templates", __dir__)
8
8
 
9
+ argument :resource, type: :string, required: false,
10
+ desc: "Resource model (e.g., User) to eject a resource-specific edit view"
11
+
9
12
  class_option :namespace, type: :string, default: "admin",
10
13
  desc: "Admin namespace"
11
14
 
12
15
  def copy_edit_template
13
- copy_file "pages/edit.jsx", "app/views/#{namespace_name}/application/edit.jsx"
16
+ if resource.present?
17
+ eject_json_props
18
+ copy_file "pages/edit.jsx", "app/views/#{namespace_name}/#{resource_path}/edit.jsx"
19
+ copy_file "pages/_form.jsx", "app/views/#{namespace_name}/#{resource_path}/_form.jsx"
20
+ eject_new_view if should_eject_new?
21
+ else
22
+ copy_file "pages/edit.jsx", "app/views/#{namespace_name}/application/edit.jsx"
23
+ copy_file "pages/_form.jsx", "app/views/#{namespace_name}/application/_form.jsx"
24
+ eject_new_view if should_eject_new?
25
+ end
14
26
  end
15
27
 
16
- def copy_form_partial
17
- copy_file "pages/_form.jsx", "app/views/#{namespace_name}/application/_form.jsx"
28
+ private
29
+
30
+ def eject_json_props
31
+ create_file "app/views/#{namespace_name}/#{resource_path}/edit.json.props", <<~RUBY
32
+ json.partial! partial: "terrazzo/application/edit_base"
33
+ # Add custom props below:
34
+ # json.customProp @resource.some_method
35
+ RUBY
18
36
  end
19
37
 
20
- private
38
+ def should_eject_new?
39
+ new_path = if resource.present?
40
+ "app/views/#{namespace_name}/#{resource_path}/new.jsx"
41
+ else
42
+ "app/views/#{namespace_name}/application/new.jsx"
43
+ end
44
+ return false if File.exist?(new_path)
45
+
46
+ yes?("Also eject the new view to share the custom form partial? (y/n)")
47
+ end
48
+
49
+ def eject_new_view
50
+ if resource.present?
51
+ eject_new_json_props
52
+ copy_file "pages/new.jsx", "app/views/#{namespace_name}/#{resource_path}/new.jsx"
53
+ else
54
+ copy_file "pages/new.jsx", "app/views/#{namespace_name}/application/new.jsx"
55
+ end
56
+ end
57
+
58
+ def eject_new_json_props
59
+ create_file "app/views/#{namespace_name}/#{resource_path}/new.json.props", <<~RUBY
60
+ json.partial! partial: "terrazzo/application/new_base"
61
+ # Add custom props below:
62
+ # json.customProp SomeModel.some_value
63
+ RUBY
64
+ end
65
+
66
+ def resource_path
67
+ resource.underscore.pluralize
68
+ end
21
69
 
22
70
  def namespace_name
23
71
  options[:namespace]
@@ -6,15 +6,37 @@ module Terrazzo
6
6
  class IndexGenerator < Rails::Generators::Base
7
7
  source_root File.expand_path("templates", __dir__)
8
8
 
9
+ argument :resource, type: :string, required: false,
10
+ desc: "Resource model (e.g., User) to eject a resource-specific index view"
11
+
9
12
  class_option :namespace, type: :string, default: "admin",
10
13
  desc: "Admin namespace"
11
14
 
12
15
  def copy_index_template
13
- copy_file "pages/index.jsx", "app/views/#{namespace_name}/application/index.jsx"
16
+ if resource.present?
17
+ eject_json_props
18
+ copy_file "pages/index.jsx", "app/views/#{namespace_name}/#{resource_path}/index.jsx"
19
+ copy_file "pages/_collection.jsx", "app/views/#{namespace_name}/#{resource_path}/_collection.jsx"
20
+ else
21
+ copy_file "pages/index.jsx", "app/views/#{namespace_name}/application/index.jsx"
22
+ copy_file "pages/_collection.jsx", "app/views/#{namespace_name}/application/_collection.jsx"
23
+ end
14
24
  end
15
25
 
16
26
  private
17
27
 
28
+ def eject_json_props
29
+ create_file "app/views/#{namespace_name}/#{resource_path}/index.json.props", <<~RUBY
30
+ json.partial! partial: "terrazzo/application/index_base"
31
+ # Add custom props below:
32
+ # json.customProp SomeModel.count
33
+ RUBY
34
+ end
35
+
36
+ def resource_path
37
+ resource.underscore.pluralize
38
+ end
39
+
18
40
  def namespace_name
19
41
  options[:namespace]
20
42
  end
@@ -6,18 +6,66 @@ module Terrazzo
6
6
  class NewGenerator < Rails::Generators::Base
7
7
  source_root File.expand_path("templates", __dir__)
8
8
 
9
+ argument :resource, type: :string, required: false,
10
+ desc: "Resource model (e.g., User) to eject a resource-specific new view"
11
+
9
12
  class_option :namespace, type: :string, default: "admin",
10
13
  desc: "Admin namespace"
11
14
 
12
15
  def copy_new_template
13
- copy_file "pages/new.jsx", "app/views/#{namespace_name}/application/new.jsx"
16
+ if resource.present?
17
+ eject_json_props
18
+ copy_file "pages/new.jsx", "app/views/#{namespace_name}/#{resource_path}/new.jsx"
19
+ copy_file "pages/_form.jsx", "app/views/#{namespace_name}/#{resource_path}/_form.jsx"
20
+ eject_edit_view if should_eject_edit?
21
+ else
22
+ copy_file "pages/new.jsx", "app/views/#{namespace_name}/application/new.jsx"
23
+ copy_file "pages/_form.jsx", "app/views/#{namespace_name}/application/_form.jsx"
24
+ eject_edit_view if should_eject_edit?
25
+ end
14
26
  end
15
27
 
16
- def copy_form_partial
17
- copy_file "pages/_form.jsx", "app/views/#{namespace_name}/application/_form.jsx"
28
+ private
29
+
30
+ def eject_json_props
31
+ create_file "app/views/#{namespace_name}/#{resource_path}/new.json.props", <<~RUBY
32
+ json.partial! partial: "terrazzo/application/new_base"
33
+ # Add custom props below:
34
+ # json.customProp SomeModel.some_value
35
+ RUBY
18
36
  end
19
37
 
20
- private
38
+ def should_eject_edit?
39
+ edit_path = if resource.present?
40
+ "app/views/#{namespace_name}/#{resource_path}/edit.jsx"
41
+ else
42
+ "app/views/#{namespace_name}/application/edit.jsx"
43
+ end
44
+ return false if File.exist?(edit_path)
45
+
46
+ yes?("Also eject the edit view to share the custom form partial? (y/n)")
47
+ end
48
+
49
+ def eject_edit_view
50
+ if resource.present?
51
+ eject_edit_json_props
52
+ copy_file "pages/edit.jsx", "app/views/#{namespace_name}/#{resource_path}/edit.jsx"
53
+ else
54
+ copy_file "pages/edit.jsx", "app/views/#{namespace_name}/application/edit.jsx"
55
+ end
56
+ end
57
+
58
+ def eject_edit_json_props
59
+ create_file "app/views/#{namespace_name}/#{resource_path}/edit.json.props", <<~RUBY
60
+ json.partial! partial: "terrazzo/application/edit_base"
61
+ # Add custom props below:
62
+ # json.customProp @resource.some_method
63
+ RUBY
64
+ end
65
+
66
+ def resource_path
67
+ resource.underscore.pluralize
68
+ end
21
69
 
22
70
  def namespace_name
23
71
  options[:namespace]
@@ -6,15 +6,34 @@ module Terrazzo
6
6
  class ShowGenerator < Rails::Generators::Base
7
7
  source_root File.expand_path("templates", __dir__)
8
8
 
9
+ argument :resource, type: :string, required: false,
10
+ desc: "Resource model (e.g., User) to eject a resource-specific show.json.props"
11
+
9
12
  class_option :namespace, type: :string, default: "admin",
10
13
  desc: "Admin namespace"
11
14
 
12
15
  def copy_show_template
13
- copy_file "pages/show.jsx", "app/views/#{namespace_name}/application/show.jsx"
16
+ if resource.present?
17
+ eject_json_props
18
+ else
19
+ copy_file "pages/show.jsx", "app/views/#{namespace_name}/application/show.jsx"
20
+ end
14
21
  end
15
22
 
16
23
  private
17
24
 
25
+ def eject_json_props
26
+ create_file "app/views/#{namespace_name}/#{resource_path}/show.json.props", <<~RUBY
27
+ json.partial! partial: "terrazzo/application/show_base"
28
+ # Add custom props below:
29
+ # json.customProp @resource.some_method
30
+ RUBY
31
+ end
32
+
33
+ def resource_path
34
+ resource.underscore.pluralize
35
+ end
36
+
18
37
  def namespace_name
19
38
  options[:namespace]
20
39
  end
@@ -11,9 +11,10 @@ import {
11
11
  } from "terrazzo/ui";
12
12
  import { Badge } from "terrazzo/ui";
13
13
  import { Button } from "terrazzo/ui";
14
+ import { CollectionItemActions } from "terrazzo/components";
14
15
  import { FieldRenderer } from "../FieldRenderer";
15
16
 
16
- export function ShowField({ value, itemShowPaths }) {
17
+ export function ShowField({ value, itemShowPaths, collectionItemActions }) {
17
18
  if (!value) return <span className="text-muted-foreground">None</span>;
18
19
 
19
20
  const { items, headers, total, initialLimit } = value;
@@ -46,6 +47,7 @@ export function ShowField({ value, itemShowPaths }) {
46
47
  {headers.map((header) =>
47
48
  <TableHead key={header.attribute}>{header.label}</TableHead>
48
49
  )}
50
+ {collectionItemActions && <TableHead></TableHead>}
49
51
  </TableRow>
50
52
  </TableHeader>
51
53
  <TableBody>
@@ -67,6 +69,11 @@ export function ShowField({ value, itemShowPaths }) {
67
69
  )}
68
70
  </TableCell>
69
71
  )}
72
+ {collectionItemActions && (
73
+ <TableCell>
74
+ <CollectionItemActions actions={collectionItemActions?.[String(item.id)]} />
75
+ </TableCell>
76
+ )}
70
77
  </TableRow>
71
78
  );
72
79
  })}
@@ -0,0 +1,55 @@
1
+ import React, { useContext } from "react";
2
+ import { NavigationContext } from "@thoughtbot/superglue";
3
+
4
+ import { SortableHeader, CollectionItemActions } from "../components";
5
+ import { FieldRenderer } from "../fields";
6
+ import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "../components/ui";
7
+
8
+ export function AdminCollection({ table }) {
9
+ const { visit } = useContext(NavigationContext);
10
+
11
+ const handleRowClick = (e, showPath) => {
12
+ if (!showPath) return;
13
+ if (e.target.closest("a, button, form")) return;
14
+ if (window.getSelection().toString()) return;
15
+ visit(showPath, {});
16
+ };
17
+
18
+ return (
19
+ <div className="overflow-x-auto rounded-md border">
20
+ <Table>
21
+ <TableHeader>
22
+ <TableRow>
23
+ {table.headers.map((header) =>
24
+ <SortableHeader key={header.attribute} {...header} />
25
+ )}
26
+ <TableHead className="w-[120px]">Actions</TableHead>
27
+ </TableRow>
28
+ </TableHeader>
29
+ <TableBody>
30
+ {table.rows.map((row) =>
31
+ <TableRow
32
+ key={row.id}
33
+ className={row.showPath ? "cursor-pointer" : ""}
34
+ onClick={(e) => handleRowClick(e, row.showPath)}>
35
+ {row.cells.map((cell) =>
36
+ <TableCell key={cell.attribute}>
37
+ {cell.showPath ? (
38
+ <a href={cell.showPath} data-sg-visit className="hover:underline">
39
+ <FieldRenderer mode="index" {...cell} />
40
+ </a>
41
+ ) : (
42
+ <FieldRenderer mode="index" {...cell} />
43
+ )}
44
+ </TableCell>
45
+ )}
46
+ <TableCell>
47
+ <CollectionItemActions actions={row.collectionItemActions} />
48
+ </TableCell>
49
+ </TableRow>
50
+ )}
51
+ </TableBody>
52
+ </Table>
53
+ </div>
54
+ );
55
+ }
@@ -1,12 +1,11 @@
1
- import React, { useContext } from "react";
2
- import { useContent, NavigationContext } from "@thoughtbot/superglue";
1
+ import React from "react";
2
+ import { useContent } from "@thoughtbot/superglue";
3
3
 
4
- import { Layout, SearchBar, Pagination, SortableHeader } from "terrazzo/components";
5
- import { FieldRenderer } from "terrazzo/fields";
6
- import { Button, Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "terrazzo/ui";
4
+ import { Layout, SearchBar, Pagination } from "../components";
5
+ import { AdminCollection } from "./_collection";
6
+ import { Button } from "../components/ui";
7
7
 
8
8
  export default function AdminIndex() {
9
- const { visit } = useContext(NavigationContext);
10
9
  const {
11
10
  table,
12
11
  searchBar,
@@ -17,18 +16,11 @@ export default function AdminIndex() {
17
16
  singularResourceName
18
17
  } = useContent();
19
18
 
20
- const handleRowClick = (e, showPath) => {
21
- if (!showPath) return;
22
- if (e.target.closest("a, button, form")) return;
23
- if (window.getSelection().toString()) return;
24
- visit(showPath, {});
25
- };
26
-
27
19
  return (
28
20
  <Layout
29
21
  navigation={navigation}
30
22
  title={resourceName}
31
- actions={
23
+ actions={newResourcePath &&
32
24
  <a href={newResourcePath} data-sg-visit>
33
25
  <Button size="sm">New {singularResourceName}</Button>
34
26
  </a>
@@ -38,73 +30,7 @@ export default function AdminIndex() {
38
30
  <SearchBar {...searchBar} />
39
31
  </div>
40
32
 
41
- <div className="overflow-x-auto rounded-md border">
42
- <Table>
43
- <TableHeader>
44
- <TableRow>
45
- {table.headers.map((header) =>
46
- <SortableHeader key={header.attribute} {...header} />
47
- )}
48
- <TableHead className="w-[120px]">Actions</TableHead>
49
- </TableRow>
50
- </TableHeader>
51
- <TableBody>
52
- {table.rows.map((row) =>
53
- <TableRow
54
- key={row.id}
55
- className={row.showPath ? "cursor-pointer" : ""}
56
- onClick={(e) => handleRowClick(e, row.showPath)}>
57
- {row.cells.map((cell) =>
58
- <TableCell key={cell.attribute}>
59
- {cell.showPath ? (
60
- <a href={cell.showPath} data-sg-visit className="hover:underline">
61
- <FieldRenderer mode="index" {...cell} />
62
- </a>
63
- ) : (
64
- <FieldRenderer mode="index" {...cell} />
65
- )}
66
- </TableCell>
67
- )}
68
- <TableCell>
69
- <div className="flex gap-1">
70
- {row.showPath &&
71
- <a href={row.showPath} data-sg-visit>
72
- <Button variant="ghost" size="sm">Show</Button>
73
- </a>
74
- }
75
- {row.editPath &&
76
- <a href={row.editPath} data-sg-visit>
77
- <Button variant="ghost" size="sm">Edit</Button>
78
- </a>
79
- }
80
- {row.deletePath &&
81
- <form
82
- action={row.deletePath}
83
- method="post"
84
- data-sg-visit
85
- style={{ display: "inline" }}
86
- onSubmit={(e) => {
87
- if (!window.confirm("Are you sure?")) e.preventDefault();
88
- }}>
89
-
90
- <input type="hidden" name="_method" value="delete" />
91
- <input
92
- type="hidden"
93
- name="authenticity_token"
94
- value={document.querySelector('meta[name="csrf-token"]')?.content ?? ""} />
95
-
96
- <Button type="submit" variant="ghost" size="sm" className="text-destructive">
97
- Delete
98
- </Button>
99
- </form>
100
- }
101
- </div>
102
- </TableCell>
103
- </TableRow>
104
- )}
105
- </TableBody>
106
- </Table>
107
- </div>
33
+ <AdminCollection table={table} />
108
34
 
109
35
  <Pagination {...pagination} />
110
36
  </Layout>);
@@ -44,7 +44,14 @@ module Terrazzo
44
44
  associated_class.all
45
45
  end
46
46
  pk = association_primary_key
47
- scope.map { |r| [display_name(r), r.public_send(pk).to_s] }
47
+ dashboard = associated_dashboard
48
+ scope.map { |r| [dashboard ? dashboard.display_resource(r) : display_name(r), r.public_send(pk).to_s] }
49
+ end
50
+
51
+ def associated_dashboard
52
+ "#{associated_class.name}Dashboard".constantize.new
53
+ rescue NameError
54
+ nil
48
55
  end
49
56
 
50
57
  def association_primary_key
@@ -14,7 +14,7 @@ module Terrazzo
14
14
  def serializable_options
15
15
  opts = {}
16
16
  if resource
17
- collection = resource_options
17
+ collection = ordered_resource_options
18
18
  if options[:include_blank]
19
19
  collection = [["", nil]] + collection
20
20
  end
@@ -38,6 +38,21 @@ module Terrazzo
38
38
 
39
39
  private
40
40
 
41
+ def ordered_resource_options
42
+ return [] unless associated_class
43
+ scope = if options[:scope].is_a?(Proc)
44
+ options[:scope].call(associated_class)
45
+ elsif options[:scope]
46
+ associated_class.public_send(options[:scope])
47
+ else
48
+ associated_class.all
49
+ end
50
+ scope = scope.reorder(options[:order]) if options[:order]
51
+ pk = association_primary_key
52
+ dashboard = associated_dashboard
53
+ scope.map { |r| [dashboard ? dashboard.display_resource(r) : display_name(r), r.public_send(pk).to_s] }
54
+ end
55
+
41
56
  def foreign_key_value
42
57
  return nil unless resource
43
58
 
@@ -3,8 +3,15 @@ module Terrazzo
3
3
  class Date < Base
4
4
  def serialize_value(_mode)
5
5
  return nil if data.nil?
6
+ value = if options[:timezone]
7
+ data.in_time_zone(options[:timezone]).to_date
8
+ elsif data.respond_to?(:in_time_zone)
9
+ data.in_time_zone.to_date
10
+ else
11
+ data
12
+ end
6
13
  format = options[:format]
7
- format ? data.strftime(format) : data.to_s
14
+ format ? value.strftime(format) : value.to_s
8
15
  end
9
16
  end
10
17
  end
@@ -3,8 +3,15 @@ module Terrazzo
3
3
  class DateTime < Base
4
4
  def serialize_value(_mode)
5
5
  return nil if data.nil?
6
+ value = if options[:timezone]
7
+ data.in_time_zone(options[:timezone])
8
+ elsif data.respond_to?(:in_time_zone)
9
+ data.in_time_zone
10
+ else
11
+ data
12
+ end
6
13
  format = options[:format]
7
- format ? data.strftime(format) : data.iso8601
14
+ format ? value.strftime(format) : value.iso8601
8
15
  end
9
16
  end
10
17
  end
@@ -45,7 +45,8 @@ module Terrazzo
45
45
 
46
46
  def serialize_show_value
47
47
  limit = options.fetch(:limit, 5)
48
- all_records = data.to_a
48
+ records = apply_sorting(data)
49
+ all_records = records.to_a
49
50
  total = all_records.size
50
51
  col_attrs = options[:collection_attributes] || resolve_default_collection_attributes
51
52
 
@@ -94,6 +95,29 @@ module Terrazzo
94
95
  }
95
96
  end
96
97
 
98
+ def resource_options
99
+ return [] unless associated_class
100
+ scope = if options[:scope].is_a?(Proc)
101
+ options[:scope].call(associated_class)
102
+ elsif options[:scope]
103
+ associated_class.public_send(options[:scope])
104
+ else
105
+ associated_class.all
106
+ end
107
+ scope = scope.includes(*options[:includes]) if options.key?(:includes)
108
+ pk = association_primary_key
109
+ dashboard = associated_dashboard
110
+ scope.map { |r| [dashboard ? dashboard.display_resource(r) : display_name(r), r.public_send(pk).to_s] }
111
+ end
112
+
113
+ def apply_sorting(records)
114
+ sort_by = options[:sort_by]
115
+ return records unless sort_by
116
+
117
+ direction = options.fetch(:direction, :asc)
118
+ records.reorder(sort_by => direction)
119
+ end
120
+
97
121
  def find_associated_dashboard
98
122
  klass = associated_class
99
123
  "#{klass.name}Dashboard".constantize
@@ -1,10 +1,20 @@
1
+ require "active_support/number_helper"
2
+
1
3
  module Terrazzo
2
4
  module Field
3
5
  class Number < Base
4
6
  def serialize_value(mode)
5
- return data if data.nil? || mode == :form || !options.key?(:multiplier)
7
+ return data if data.nil? || mode == :form
8
+
9
+ value = options.key?(:multiplier) ? data * options[:multiplier] : data
6
10
 
7
- data * options[:multiplier]
11
+ if options[:format]
12
+ formatter = options[:format][:formatter]
13
+ formatter_options = options[:format][:formatter_options].to_h
14
+ ActiveSupport::NumberHelper.try(formatter, value, **formatter_options) || value
15
+ else
16
+ value
17
+ end
8
18
  end
9
19
 
10
20
  def serializable_options
@@ -15,9 +15,11 @@ module Terrazzo
15
15
  def serializable_options
16
16
  opts = {}
17
17
  classes = options[:classes] || []
18
+ order = options[:order]
18
19
  opts[:groupedOptions] = classes.each_with_object({}) do |klass, hash|
19
20
  klass = klass.constantize if klass.is_a?(::String)
20
- hash[klass.name] = klass.all.map { |r| [display_name(r), r.id.to_s] }
21
+ scope = order ? klass.order(order) : klass.all
22
+ hash[klass.name] = scope.map { |r| [display_name(r), r.id.to_s] }
21
23
  end
22
24
  opts
23
25
  end
@@ -1,3 +1,3 @@
1
1
  module Terrazzo
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: terrazzo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Terrazzo Contributors
@@ -117,7 +117,12 @@ files:
117
117
  - LICENSE
118
118
  - Rakefile
119
119
  - app/controllers/terrazzo/application_controller.rb
120
+ - app/helpers/terrazzo/collection_actions_helper.rb
121
+ - app/views/terrazzo/application/_edit_base.json.props
122
+ - app/views/terrazzo/application/_index_base.json.props
120
123
  - app/views/terrazzo/application/_navigation.json.props
124
+ - app/views/terrazzo/application/_new_base.json.props
125
+ - app/views/terrazzo/application/_show_base.json.props
121
126
  - app/views/terrazzo/application/edit.json.props
122
127
  - app/views/terrazzo/application/index.json.props
123
128
  - app/views/terrazzo/application/new.json.props
@@ -232,6 +237,7 @@ files:
232
237
  - lib/generators/terrazzo/views/templates/fields/url/FormField.jsx
233
238
  - lib/generators/terrazzo/views/templates/fields/url/IndexField.jsx
234
239
  - lib/generators/terrazzo/views/templates/fields/url/ShowField.jsx
240
+ - lib/generators/terrazzo/views/templates/pages/_collection.jsx
235
241
  - lib/generators/terrazzo/views/templates/pages/_form.jsx
236
242
  - lib/generators/terrazzo/views/templates/pages/_navigation.json.props
237
243
  - lib/generators/terrazzo/views/templates/pages/edit.jsx