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 +4 -4
- data/app/controllers/terrazzo/application_controller.rb +12 -4
- data/app/helpers/terrazzo/collection_actions_helper.rb +22 -0
- data/app/views/terrazzo/application/edit.json.props +2 -1
- data/app/views/terrazzo/application/index.json.props +2 -3
- data/app/views/terrazzo/application/new.json.props +2 -1
- data/app/views/terrazzo/application/show.json.props +5 -2
- data/lib/generators/terrazzo/dashboard/dashboard_generator.rb +26 -0
- data/lib/generators/terrazzo/views/templates/fields/has_many/ShowField.jsx +8 -1
- data/lib/generators/terrazzo/views/templates/pages/index.jsx +2 -34
- data/lib/terrazzo/base_dashboard.rb +4 -1
- data/lib/terrazzo/field/asset.rb +34 -0
- data/lib/terrazzo/field/associative.rb +8 -1
- data/lib/terrazzo/field/date.rb +2 -1
- data/lib/terrazzo/field/date_time.rb +2 -1
- data/lib/terrazzo/field/has_many.rb +4 -1
- data/lib/terrazzo/field/number.rb +4 -2
- data/lib/terrazzo/field/time.rb +2 -1
- data/lib/terrazzo/version.rb +1 -1
- data/lib/terrazzo.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c7c9b21f28f73f5cf332ac0d4b21d7eb32ae2461bfb2f52c5652eecd7642a8b
|
|
4
|
+
data.tar.gz: 7185c42a9d472e4d84ff7047a36a5468f761f272b9af5e0d9cb86930b130857f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/terrazzo/field/date.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
module Terrazzo
|
|
2
2
|
module Field
|
|
3
3
|
class Number < Base
|
|
4
|
-
def serialize_value(
|
|
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
|
data/lib/terrazzo/field/time.rb
CHANGED
data/lib/terrazzo/version.rb
CHANGED
data/lib/terrazzo.rb
CHANGED
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.
|
|
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
|