terrazzo 0.3.0 → 0.3.2

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: a9ba7563c1db759831717790db2c022eb3f2d343c3f393f6679e164c250f8fc5
4
- data.tar.gz: a985e9f57b97f224b26a58cd72d9e580666d0aae1b16efd8e722ce14460ad860
3
+ metadata.gz: 7c7c9b21f28f73f5cf332ac0d4b21d7eb32ae2461bfb2f52c5652eecd7642a8b
4
+ data.tar.gz: 7185c42a9d472e4d84ff7047a36a5468f761f272b9af5e0d9cb86930b130857f
5
5
  SHA512:
6
- metadata.gz: 877fed40c106cc9ed5443197322033abd8c4d51c5fa69cb596d63ca77b355ffeb4c3235a13125a6bb6c7f1f28a747cab2c2674ebf08fee6bb4a2c7f225fc1eed
7
- data.tar.gz: 24cdb50150d40149fda75e5fe376978222ab3b200247a006989cb52e98ea2e9482747ab49b3880f7557cb072ca089691f7468f3573a778eaec764bcf45b6cf3c
6
+ metadata.gz: f751974222e4ad11bd348dc07e3d8563c67454cf96d1b6faf3054b0b77f978dbba9dd1a10b16084b544e17799d8969df3e7ee50bf4485c0b1ce5849ab8f44716
7
+ data.tar.gz: b22856b13ec0ab28ba3e689cb92e99427311834f4e0e807e1a15506488832ce33f7c90a6d982b55d1c474ff4a1837394eed02d4a9bf3882164b9da988bb3b2e8
@@ -16,7 +16,8 @@ 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
+ 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)
@@ -207,6 +205,16 @@ module Terrazzo
207
205
  "#{ns}/application/#{mapped_action}"
208
206
  end
209
207
 
208
+ def route_exists?(action)
209
+ @_route_exists_cache ||= {}
210
+ return @_route_exists_cache[action] if @_route_exists_cache.key?(action)
211
+
212
+ @_route_exists_cache[action] = Rails.application.routes.routes.any? do |route|
213
+ route.defaults[:controller] == controller_path &&
214
+ route.defaults[:action] == action.to_s
215
+ end
216
+ end
217
+
210
218
  private
211
219
 
212
220
  def resolver
@@ -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
@@ -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?
@@ -33,8 +33,7 @@ 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.collectionItemActions collection_item_actions(resource)
38
37
 
39
38
  json.cells do
40
39
  json.array! @page.attribute_names do |attr|
@@ -89,4 +88,4 @@ end
89
88
 
90
89
  json.resourceName resource_name.pluralize
91
90
  json.singularResourceName resource_name
92
- json.newResourcePath new_polymorphic_path([namespace, resource_class]) rescue nil
91
+ 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?
@@ -11,6 +11,9 @@ show_field_json = ->(json, field) do
11
11
  json.itemShowPaths(field.data.each_with_object({}) do |record, paths|
12
12
  paths[record.id.to_s] = polymorphic_path([namespace, record]) rescue nil
13
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)
14
17
  else
15
18
  json.showPath polymorphic_path([namespace, field.data]) rescue nil
16
19
  end
@@ -34,8 +37,8 @@ json.attributes do
34
37
  end
35
38
  end
36
39
 
37
- json.editPath edit_polymorphic_path([namespace, @resource]) rescue nil
38
- json.deletePath polymorphic_path([namespace, @resource]) rescue nil
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
39
42
  json.indexPath begin
40
43
  url_for(controller: controller_path, action: :index, only_path: true)
41
44
  rescue ActionController::UrlGenerationError
@@ -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
@@ -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
  })}
@@ -1,7 +1,7 @@
1
1
  import React, { useContext } from "react";
2
2
  import { useContent, NavigationContext } from "@thoughtbot/superglue";
3
3
 
4
- import { Layout, SearchBar, Pagination, SortableHeader } from "terrazzo/components";
4
+ import { Layout, SearchBar, Pagination, SortableHeader, CollectionItemActions } from "terrazzo/components";
5
5
  import { FieldRenderer } from "terrazzo/fields";
6
6
  import { Button, Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "terrazzo/ui";
7
7
 
@@ -66,39 +66,7 @@ export default function AdminIndex() {
66
66
  </TableCell>
67
67
  )}
68
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>
69
+ <CollectionItemActions actions={row.collectionItemActions} />
102
70
  </TableCell>
103
71
  </TableRow>
104
72
  )}
@@ -64,10 +64,13 @@ module Terrazzo
64
64
 
65
65
  def collection_includes
66
66
  collection_attr_set = Set.new(collection_attributes)
67
+ model = self.class.model
67
68
  attribute_types.each_with_object([]) do |(attr, type), includes|
68
69
  next unless collection_attr_set.include?(attr)
69
70
  next unless type.eager_load?
70
- next unless self.class.model.reflect_on_association(attr)
71
+ has_association = model.reflect_on_association(attr)
72
+ has_attachment = model.respond_to?(:reflect_on_attachment) && model.reflect_on_attachment(attr)
73
+ next unless has_association || has_attachment
71
74
  includes << attr
72
75
  end
73
76
  end
@@ -0,0 +1,34 @@
1
+ module Terrazzo
2
+ module Field
3
+ class Asset < Base
4
+ def serialize_value(mode)
5
+ return nil if data.nil? || !data.attached?
6
+
7
+ case mode
8
+ when :index
9
+ data.filename.to_s
10
+ when :show
11
+ { filename: data.filename.to_s, byteSize: data.byte_size, contentType: data.content_type }
12
+ when :form
13
+ { filename: data.filename.to_s, signedId: data.signed_id }
14
+ else
15
+ data.filename.to_s
16
+ end
17
+ end
18
+
19
+ class << self
20
+ def searchable?
21
+ false
22
+ end
23
+
24
+ def sortable?
25
+ false
26
+ end
27
+
28
+ def eager_load?
29
+ true
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -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
@@ -3,7 +3,8 @@ module Terrazzo
3
3
  class Date < Base
4
4
  def serialize_value(_mode)
5
5
  return nil if data.nil?
6
- data.to_s
6
+ format = options[:format]
7
+ format ? data.strftime(format) : data.to_s
7
8
  end
8
9
  end
9
10
  end
@@ -3,7 +3,8 @@ module Terrazzo
3
3
  class DateTime < Base
4
4
  def serialize_value(_mode)
5
5
  return nil if data.nil?
6
- data.iso8601
6
+ format = options[:format]
7
+ format ? data.strftime(format) : data.iso8601
7
8
  end
8
9
  end
9
10
  end
@@ -6,7 +6,10 @@ module Terrazzo
6
6
 
7
7
  case mode
8
8
  when :index
9
- data.size
9
+ count = data.size
10
+ label = attribute.to_s.humanize.downcase
11
+ label = label.singularize if count == 1
12
+ { count: count, label: label }
10
13
  when :form
11
14
  data.map { |r| r.id.to_s }
12
15
  when :show
@@ -1,8 +1,10 @@
1
1
  module Terrazzo
2
2
  module Field
3
3
  class Number < Base
4
- def serialize_value(_mode)
5
- data
4
+ def serialize_value(mode)
5
+ return data if data.nil? || mode == :form || !options.key?(:multiplier)
6
+
7
+ data * options[:multiplier]
6
8
  end
7
9
 
8
10
  def serializable_options
@@ -3,7 +3,8 @@ module Terrazzo
3
3
  class Time < Base
4
4
  def serialize_value(_mode)
5
5
  return nil if data.nil?
6
- data.to_s
6
+ format = options[:format]
7
+ format ? data.strftime(format) : data.to_s
7
8
  end
8
9
  end
9
10
  end
@@ -1,3 +1,3 @@
1
1
  module Terrazzo
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.2"
3
3
  end
data/lib/terrazzo.rb CHANGED
@@ -37,6 +37,7 @@ module Terrazzo
37
37
  autoload :HasMany, "terrazzo/field/has_many"
38
38
  autoload :HasOne, "terrazzo/field/has_one"
39
39
  autoload :Polymorphic, "terrazzo/field/polymorphic"
40
+ autoload :Asset, "terrazzo/field/asset"
40
41
  end
41
42
 
42
43
  module Page
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.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Terrazzo Contributors
@@ -117,6 +117,7 @@ files:
117
117
  - LICENSE
118
118
  - Rakefile
119
119
  - app/controllers/terrazzo/application_controller.rb
120
+ - app/helpers/terrazzo/collection_actions_helper.rb
120
121
  - app/views/terrazzo/application/_navigation.json.props
121
122
  - app/views/terrazzo/application/edit.json.props
122
123
  - app/views/terrazzo/application/index.json.props
@@ -242,6 +243,7 @@ files:
242
243
  - lib/terrazzo.rb
243
244
  - lib/terrazzo/base_dashboard.rb
244
245
  - lib/terrazzo/engine.rb
246
+ - lib/terrazzo/field/asset.rb
245
247
  - lib/terrazzo/field/associative.rb
246
248
  - lib/terrazzo/field/base.rb
247
249
  - lib/terrazzo/field/belongs_to.rb