terrazzo 0.1.0 → 0.2.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 +2 -0
- data/app/views/terrazzo/application/index.json.props +4 -0
- data/app/views/terrazzo/application/show.json.props +20 -10
- data/lib/generators/terrazzo/dashboard/dashboard_generator.rb +3 -3
- data/lib/generators/terrazzo/install/install_generator.rb +15 -4
- data/lib/generators/terrazzo/install/templates/admin.css +119 -0
- data/lib/generators/terrazzo/install/templates/application_visit.js.erb +45 -3
- data/lib/generators/terrazzo/routes/routes_generator.rb +45 -12
- data/lib/generators/terrazzo/views/templates/components/Layout.jsx +2 -0
- data/lib/generators/terrazzo/views/templates/components/SortableHeader.jsx +4 -4
- data/lib/generators/terrazzo/views/templates/fields/has_many/ShowField.jsx +55 -24
- data/lib/generators/terrazzo/views/templates/pages/index.jsx +21 -4
- data/lib/generators/terrazzo/views/templates/pages/show.jsx +7 -1
- data/lib/terrazzo/base_dashboard.rb +2 -0
- data/lib/terrazzo/engine.rb +6 -0
- data/lib/terrazzo/field/base.rb +1 -1
- data/lib/terrazzo/field/deferred.rb +2 -2
- data/lib/terrazzo/field/email.rb +0 -6
- data/lib/terrazzo/field/has_many.rb +6 -11
- data/lib/terrazzo/field/string.rb +0 -4
- data/lib/terrazzo/field/text.rb +0 -4
- data/lib/terrazzo/uses_superglue.rb +41 -0
- 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: b55338dd365f9ad475e3892c34a4a5ca706625b50bd331dd6793bbd756ec75e7
|
|
4
|
+
data.tar.gz: a5193fda7a10ca72103816e0f47a58ba7facf375003a4af22efa0385914378b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d048907fe5b71cee9100ce4cd7a1b606e666972be105a39f15c24f3119531deccc4bc7cb85144b65623faf933dbebfd1f78d61b841909872046afe754c3ea71f
|
|
7
|
+
data.tar.gz: 6eb4e86c0a06cd790094cc0829313894f2c9d7b6ce232327a5c46610d9d507c5215b87ec81c3a136c0e5ac2b6841c22de4f81b0544a46aba2846a1f8e762f009
|
|
@@ -14,6 +14,8 @@ module Terrazzo
|
|
|
14
14
|
|
|
15
15
|
before_action :use_jsx_rendering_defaults
|
|
16
16
|
|
|
17
|
+
prepend Terrazzo::UsesSuperglue::TemplateLookupOverride
|
|
18
|
+
|
|
17
19
|
helper_method :namespace, :dashboard, :resource_name, :resource_class, :application_title, :terrazzo_page_identifier
|
|
18
20
|
|
|
19
21
|
def index
|
|
@@ -43,6 +43,10 @@ json.table do
|
|
|
43
43
|
json.fieldType field.field_type
|
|
44
44
|
json.value field.serialize_value(:index)
|
|
45
45
|
json.options field.serializable_options
|
|
46
|
+
|
|
47
|
+
if field.class.associative? && field.data.present?
|
|
48
|
+
json.showPath polymorphic_path([namespace, field.data]) rescue nil
|
|
49
|
+
end
|
|
46
50
|
end
|
|
47
51
|
end
|
|
48
52
|
end
|
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
json.pageTitle @page.page_title
|
|
2
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
|
+
json.options field.serializable_options
|
|
9
|
+
|
|
10
|
+
if field.class.associative? && field.data.present?
|
|
11
|
+
if field.is_a?(Terrazzo::Field::HasMany)
|
|
12
|
+
json.itemShowPaths(field.data.each_with_object({}) do |record, paths|
|
|
13
|
+
paths[record.id.to_s] = polymorphic_path([namespace, record]) rescue nil
|
|
14
|
+
end)
|
|
15
|
+
else
|
|
16
|
+
json.showPath polymorphic_path([namespace, field.data]) rescue nil
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
3
21
|
json.attributeGroups do
|
|
4
22
|
json.array! @page.grouped_attributes do |group|
|
|
5
23
|
json.name group[:name]
|
|
6
24
|
json.attributes do
|
|
7
25
|
json.array! group[:fields] do |field|
|
|
8
|
-
json
|
|
9
|
-
json.label field.attribute.to_s.humanize
|
|
10
|
-
json.fieldType field.field_type
|
|
11
|
-
json.value field.serialize_value(:show)
|
|
12
|
-
json.options field.serializable_options
|
|
26
|
+
show_field_json.call(json, field)
|
|
13
27
|
end
|
|
14
28
|
end
|
|
15
29
|
end
|
|
@@ -17,11 +31,7 @@ end
|
|
|
17
31
|
|
|
18
32
|
json.attributes do
|
|
19
33
|
json.array! @page.attributes do |field|
|
|
20
|
-
json
|
|
21
|
-
json.label field.attribute.to_s.humanize
|
|
22
|
-
json.fieldType field.field_type
|
|
23
|
-
json.value field.serialize_value(:show)
|
|
24
|
-
json.options field.serializable_options
|
|
34
|
+
show_field_json.call(json, field)
|
|
25
35
|
end
|
|
26
36
|
end
|
|
27
37
|
|
|
@@ -15,12 +15,12 @@ module Terrazzo
|
|
|
15
15
|
|
|
16
16
|
def create_dashboard
|
|
17
17
|
template "dashboard.rb.erb",
|
|
18
|
-
"app/dashboards/#{file_name}_dashboard.rb"
|
|
18
|
+
"app/dashboards/#{class_path.join('/')}/#{file_name}_dashboard.rb".squeeze("/")
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def create_controller
|
|
22
22
|
template "controller.rb.erb",
|
|
23
|
-
"app/controllers/#{options[:namespace]}/#{file_name.pluralize}_controller.rb"
|
|
23
|
+
"app/controllers/#{options[:namespace]}/#{class_path.join('/')}/#{file_name.pluralize}_controller.rb".squeeze("/")
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def update_page_to_page_mapping
|
|
@@ -75,7 +75,7 @@ module Terrazzo
|
|
|
75
75
|
associations.each do |assoc|
|
|
76
76
|
case assoc.macro
|
|
77
77
|
when :belongs_to
|
|
78
|
-
types[assoc.name] = "Field::BelongsTo"
|
|
78
|
+
types[assoc.name] = assoc.options[:polymorphic] ? "Field::Polymorphic" : "Field::BelongsTo"
|
|
79
79
|
when :has_many, :has_and_belongs_to_many
|
|
80
80
|
types[assoc.name] = "Field::HasMany"
|
|
81
81
|
when :has_one
|
|
@@ -50,6 +50,11 @@ module Terrazzo
|
|
|
50
50
|
"app/javascript/#{namespace_name}/slices/flash.js"
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
def create_stylesheet
|
|
54
|
+
copy_file "admin.css",
|
|
55
|
+
"app/assets/stylesheets/#{namespace_name}.css"
|
|
56
|
+
end
|
|
57
|
+
|
|
53
58
|
def run_views_generator
|
|
54
59
|
generate "terrazzo:views", "--namespace=#{namespace_name}"
|
|
55
60
|
end
|
|
@@ -75,10 +80,16 @@ module Terrazzo
|
|
|
75
80
|
end
|
|
76
81
|
|
|
77
82
|
def application_models
|
|
78
|
-
Rails.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
[]
|
|
83
|
+
models_path = Rails.root.join("app", "models")
|
|
84
|
+
return [] unless models_path.exist?
|
|
85
|
+
|
|
86
|
+
Dir[models_path.join("**", "*.rb")].filter_map do |file|
|
|
87
|
+
relative = Pathname.new(file).relative_path_from(models_path).to_s
|
|
88
|
+
next if relative.start_with?("concerns/")
|
|
89
|
+
next if relative == "application_record.rb"
|
|
90
|
+
|
|
91
|
+
relative.delete_suffix(".rb").camelize.safe_constantize
|
|
92
|
+
end.select { |klass| klass < ApplicationRecord && !klass.abstract_class? }
|
|
82
93
|
end
|
|
83
94
|
end
|
|
84
95
|
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@custom-variant dark (&:is(.dark *));
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
--radius: 0.625rem;
|
|
7
|
+
--background: oklch(1 0 0);
|
|
8
|
+
--foreground: oklch(0.145 0 0);
|
|
9
|
+
--card: oklch(1 0 0);
|
|
10
|
+
--card-foreground: oklch(0.145 0 0);
|
|
11
|
+
--popover: oklch(1 0 0);
|
|
12
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
13
|
+
--primary: oklch(0.205 0 0);
|
|
14
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
15
|
+
--secondary: oklch(0.97 0 0);
|
|
16
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
17
|
+
--muted: oklch(0.97 0 0);
|
|
18
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
19
|
+
--accent: oklch(0.97 0 0);
|
|
20
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
21
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
22
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
23
|
+
--border: oklch(0.922 0 0);
|
|
24
|
+
--input: oklch(0.922 0 0);
|
|
25
|
+
--ring: oklch(0.708 0 0);
|
|
26
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
27
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
28
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
29
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
30
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
31
|
+
--sidebar: oklch(0.985 0 0);
|
|
32
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
33
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
34
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
35
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
36
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
37
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
38
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.dark {
|
|
42
|
+
--background: oklch(0.145 0 0);
|
|
43
|
+
--foreground: oklch(0.985 0 0);
|
|
44
|
+
--card: oklch(0.205 0 0);
|
|
45
|
+
--card-foreground: oklch(0.985 0 0);
|
|
46
|
+
--popover: oklch(0.269 0 0);
|
|
47
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
48
|
+
--primary: oklch(0.922 0 0);
|
|
49
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
50
|
+
--secondary: oklch(0.269 0 0);
|
|
51
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
52
|
+
--muted: oklch(0.269 0 0);
|
|
53
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
54
|
+
--accent: oklch(0.371 0 0);
|
|
55
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
56
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
57
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
58
|
+
--border: oklch(1 0 0 / 10%);
|
|
59
|
+
--input: oklch(1 0 0 / 15%);
|
|
60
|
+
--ring: oklch(0.556 0 0);
|
|
61
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
62
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
63
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
64
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
65
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
66
|
+
--sidebar: oklch(0.205 0 0);
|
|
67
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
68
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
69
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
70
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
71
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
72
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
73
|
+
--sidebar-ring: oklch(0.439 0 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@theme inline {
|
|
77
|
+
--color-background: var(--background);
|
|
78
|
+
--color-foreground: var(--foreground);
|
|
79
|
+
--color-card: var(--card);
|
|
80
|
+
--color-card-foreground: var(--card-foreground);
|
|
81
|
+
--color-popover: var(--popover);
|
|
82
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
83
|
+
--color-primary: var(--primary);
|
|
84
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
85
|
+
--color-secondary: var(--secondary);
|
|
86
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
87
|
+
--color-muted: var(--muted);
|
|
88
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
89
|
+
--color-accent: var(--accent);
|
|
90
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
91
|
+
--color-destructive: var(--destructive);
|
|
92
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
93
|
+
--color-border: var(--border);
|
|
94
|
+
--color-input: var(--input);
|
|
95
|
+
--color-ring: var(--ring);
|
|
96
|
+
--color-chart-1: var(--chart-1);
|
|
97
|
+
--color-chart-2: var(--chart-2);
|
|
98
|
+
--color-chart-3: var(--chart-3);
|
|
99
|
+
--color-chart-4: var(--chart-4);
|
|
100
|
+
--color-chart-5: var(--chart-5);
|
|
101
|
+
--color-sidebar: var(--sidebar);
|
|
102
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
103
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
104
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
105
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
106
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
107
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
108
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
109
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
110
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
111
|
+
--radius-lg: var(--radius);
|
|
112
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
*,
|
|
116
|
+
::after,
|
|
117
|
+
::before {
|
|
118
|
+
border-color: var(--color-border);
|
|
119
|
+
}
|
|
@@ -1,8 +1,50 @@
|
|
|
1
1
|
import { visit, remote } from "@thoughtbot/superglue/action_creators"
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
const
|
|
5
|
-
|
|
3
|
+
export const buildVisitAndRemote = (ref, store) => {
|
|
4
|
+
const appRemote = (path, { dataset, ...options }) => {
|
|
5
|
+
return store.dispatch(remote(path, options))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const appVisit = (path, { dataset, ...options } = {}) => {
|
|
9
|
+
return store
|
|
10
|
+
.dispatch(visit(path, options))
|
|
11
|
+
.then((meta) => {
|
|
12
|
+
if (meta.needsRefresh) {
|
|
13
|
+
window.location.href = meta.pageKey
|
|
14
|
+
return meta
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const navigationAction = !!dataset?.sgReplace
|
|
18
|
+
? "replace"
|
|
19
|
+
: meta.navigationAction
|
|
20
|
+
ref.current?.navigateTo(meta.pageKey, {
|
|
21
|
+
action: navigationAction,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return meta
|
|
25
|
+
})
|
|
26
|
+
.catch((err) => {
|
|
27
|
+
const response = err.response
|
|
28
|
+
|
|
29
|
+
if (!response) {
|
|
30
|
+
console.error(err)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (response.ok) {
|
|
35
|
+
window.location = response.url
|
|
36
|
+
} else {
|
|
37
|
+
if (response.status >= 400 && response.status < 500) {
|
|
38
|
+
window.location.href = "/400.html"
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
if (response.status >= 500) {
|
|
42
|
+
window.location.href = "/500.html"
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
}
|
|
6
48
|
|
|
7
49
|
return { visit: appVisit, remote: appRemote }
|
|
8
50
|
}
|
|
@@ -10,28 +10,61 @@ module Terrazzo
|
|
|
10
10
|
|
|
11
11
|
def insert_routes
|
|
12
12
|
namespace_name = options[:namespace]
|
|
13
|
-
|
|
13
|
+
models = application_models
|
|
14
|
+
|
|
15
|
+
# Group models by their module namespace
|
|
16
|
+
namespaced = {}
|
|
17
|
+
top_level = []
|
|
18
|
+
|
|
19
|
+
models.each do |model|
|
|
20
|
+
parts = model.name.split("::")
|
|
21
|
+
if parts.size > 1
|
|
22
|
+
ns = parts[0..-2].join("::").underscore
|
|
23
|
+
resource = parts.last.underscore.pluralize
|
|
24
|
+
namespaced[ns] ||= []
|
|
25
|
+
namespaced[ns] << resource
|
|
26
|
+
else
|
|
27
|
+
top_level << model.model_name.plural
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
lines = []
|
|
32
|
+
top_level.sort.each { |r| lines << " resources :#{r}" }
|
|
33
|
+
|
|
34
|
+
namespaced.sort.each do |ns, resources|
|
|
35
|
+
lines << ""
|
|
36
|
+
lines << " namespace :#{ns} do"
|
|
37
|
+
resources.sort.each { |r| lines << " resources :#{r}" }
|
|
38
|
+
lines << " end"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
first_resource = top_level.sort.first || namespaced.values.flatten.first || "dashboard"
|
|
42
|
+
|
|
43
|
+
route_block = <<~RUBY.indent(2)
|
|
14
44
|
namespace :#{namespace_name} do
|
|
45
|
+
#{lines.join("\n")}
|
|
46
|
+
|
|
15
47
|
root to: "#{first_resource}#index"
|
|
16
48
|
end
|
|
17
49
|
RUBY
|
|
18
50
|
|
|
19
|
-
route
|
|
51
|
+
route route_block.strip
|
|
20
52
|
end
|
|
21
53
|
|
|
22
54
|
private
|
|
23
55
|
|
|
24
|
-
def first_resource
|
|
25
|
-
# Default to "customers" if no models found
|
|
26
|
-
models = application_models
|
|
27
|
-
models.any? ? models.first.model_name.plural : "dashboard"
|
|
28
|
-
end
|
|
29
|
-
|
|
30
56
|
def application_models
|
|
31
|
-
Rails.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
[]
|
|
57
|
+
models_path = Rails.root.join("app", "models")
|
|
58
|
+
return [] unless models_path.exist?
|
|
59
|
+
|
|
60
|
+
Dir[models_path.join("**", "*.rb")].filter_map do |file|
|
|
61
|
+
relative = Pathname.new(file).relative_path_from(models_path).to_s
|
|
62
|
+
next if relative.start_with?("concerns/")
|
|
63
|
+
next if relative == "application_record.rb"
|
|
64
|
+
|
|
65
|
+
relative.delete_suffix(".rb").camelize.safe_constantize
|
|
66
|
+
end.select { |klass| klass < ApplicationRecord && !klass.abstract_class? }
|
|
67
|
+
.sort_by { |klass| klass.name }
|
|
35
68
|
end
|
|
36
69
|
end
|
|
37
70
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
import { TableHead } from "./ui/table";
|
|
5
5
|
|
|
@@ -17,11 +17,11 @@ export function SortableHeader({ label, sortable, sortUrl, sortDirection }) {
|
|
|
17
17
|
|
|
18
18
|
{label}
|
|
19
19
|
{sortDirection === "asc" ?
|
|
20
|
-
<
|
|
20
|
+
<ChevronUp className="h-4 w-4" /> :
|
|
21
21
|
sortDirection === "desc" ?
|
|
22
|
-
<
|
|
22
|
+
<ChevronDown className="h-4 w-4" /> :
|
|
23
23
|
|
|
24
|
-
<
|
|
24
|
+
<ChevronsUpDown className="h-4 w-4" />
|
|
25
25
|
}
|
|
26
26
|
</a>
|
|
27
27
|
</TableHead>);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useState } from "react";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
Table,
|
|
@@ -8,17 +8,24 @@ import {
|
|
|
8
8
|
TableHead,
|
|
9
9
|
TableCell,
|
|
10
10
|
} from "../../components/ui/table";
|
|
11
|
+
import { Badge } from "../../components/ui/badge";
|
|
12
|
+
import { Button } from "../../components/ui/button";
|
|
11
13
|
import { FieldRenderer } from "../FieldRenderer";
|
|
12
14
|
|
|
13
|
-
export function ShowField({ value }) {
|
|
15
|
+
export function ShowField({ value, itemShowPaths }) {
|
|
14
16
|
if (!value) return <span className="text-muted-foreground">None</span>;
|
|
15
17
|
|
|
16
|
-
const { items, headers, total,
|
|
18
|
+
const { items, headers, total, initialLimit } = value;
|
|
19
|
+
const [expanded, setExpanded] = useState(false);
|
|
17
20
|
|
|
18
21
|
if (!items || items.length === 0) {
|
|
19
22
|
return <span className="text-muted-foreground">None</span>;
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
const pathFor = (id) => itemShowPaths?.[String(id)];
|
|
26
|
+
const hasMore = initialLimit && initialLimit > 0 && total > initialLimit;
|
|
27
|
+
const visibleItems = expanded || !hasMore ? items : items.slice(0, initialLimit);
|
|
28
|
+
|
|
22
29
|
// Table mode: collection_attributes specified
|
|
23
30
|
if (headers) {
|
|
24
31
|
return (
|
|
@@ -33,22 +40,35 @@ export function ShowField({ value }) {
|
|
|
33
40
|
</TableRow>
|
|
34
41
|
</TableHeader>
|
|
35
42
|
<TableBody>
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
{visibleItems.map((item) => {
|
|
44
|
+
const showPath = pathFor(item.id);
|
|
45
|
+
return (
|
|
46
|
+
<TableRow key={item.id}>
|
|
47
|
+
{item.columns.map((col, colIndex) =>
|
|
48
|
+
<TableCell key={col.attribute}>
|
|
49
|
+
{showPath && colIndex === 0 ? (
|
|
50
|
+
<a href={showPath} data-sg-visit className="hover:underline">
|
|
51
|
+
<FieldRenderer mode="index" {...col} />
|
|
52
|
+
</a>
|
|
53
|
+
) : (
|
|
54
|
+
<FieldRenderer mode="index" {...col} />
|
|
55
|
+
)}
|
|
56
|
+
</TableCell>
|
|
57
|
+
)}
|
|
58
|
+
</TableRow>
|
|
59
|
+
);
|
|
60
|
+
})}
|
|
45
61
|
</TableBody>
|
|
46
62
|
</Table>
|
|
47
63
|
</div>
|
|
48
64
|
{hasMore && (
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
<Button
|
|
66
|
+
variant="link"
|
|
67
|
+
size="sm"
|
|
68
|
+
className="mt-2 px-0"
|
|
69
|
+
onClick={() => setExpanded(!expanded)}>
|
|
70
|
+
{expanded ? "Show less" : `Show ${total - initialLimit} more`}
|
|
71
|
+
</Button>
|
|
52
72
|
)}
|
|
53
73
|
</div>
|
|
54
74
|
);
|
|
@@ -57,16 +77,27 @@ export function ShowField({ value }) {
|
|
|
57
77
|
// Simple list mode
|
|
58
78
|
return (
|
|
59
79
|
<div>
|
|
60
|
-
<
|
|
61
|
-
{
|
|
62
|
-
|
|
80
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
81
|
+
{visibleItems.map((item) => {
|
|
82
|
+
const showPath = pathFor(item.id);
|
|
83
|
+
return showPath ? (
|
|
84
|
+
<a key={item.id} href={showPath} data-sg-visit>
|
|
85
|
+
<Badge variant="secondary" className="cursor-pointer">{item.display}</Badge>
|
|
86
|
+
</a>
|
|
87
|
+
) : (
|
|
88
|
+
<Badge key={item.id} variant="secondary">{item.display}</Badge>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
91
|
+
{hasMore && (
|
|
92
|
+
<Button
|
|
93
|
+
variant="link"
|
|
94
|
+
size="sm"
|
|
95
|
+
className="px-0"
|
|
96
|
+
onClick={() => setExpanded(!expanded)}>
|
|
97
|
+
{expanded ? "Show less" : `Show ${total - initialLimit} more`}
|
|
98
|
+
</Button>
|
|
63
99
|
)}
|
|
64
|
-
</
|
|
65
|
-
{hasMore && (
|
|
66
|
-
<p className="text-sm text-muted-foreground mt-2">
|
|
67
|
-
Showing {items.length} of {total}
|
|
68
|
-
</p>
|
|
69
|
-
)}
|
|
100
|
+
</div>
|
|
70
101
|
</div>
|
|
71
102
|
);
|
|
72
103
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { useContent } from "@thoughtbot/superglue";
|
|
1
|
+
import React, { useContext } from "react";
|
|
2
|
+
import { useContent, NavigationContext } from "@thoughtbot/superglue";
|
|
3
3
|
|
|
4
4
|
import { Layout } from "../components/Layout";
|
|
5
5
|
import { SearchBar } from "../components/SearchBar";
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "../components/ui/table";
|
|
18
18
|
|
|
19
19
|
export default function AdminIndex() {
|
|
20
|
+
const { visit } = useContext(NavigationContext);
|
|
20
21
|
const {
|
|
21
22
|
table,
|
|
22
23
|
searchBar,
|
|
@@ -27,6 +28,13 @@ export default function AdminIndex() {
|
|
|
27
28
|
singularResourceName
|
|
28
29
|
} = useContent();
|
|
29
30
|
|
|
31
|
+
const handleRowClick = (e, showPath) => {
|
|
32
|
+
if (!showPath) return;
|
|
33
|
+
if (e.target.closest("a, button, form")) return;
|
|
34
|
+
if (window.getSelection().toString()) return;
|
|
35
|
+
visit(showPath, {});
|
|
36
|
+
};
|
|
37
|
+
|
|
30
38
|
return (
|
|
31
39
|
<Layout
|
|
32
40
|
navigation={navigation}
|
|
@@ -53,10 +61,19 @@ export default function AdminIndex() {
|
|
|
53
61
|
</TableHeader>
|
|
54
62
|
<TableBody>
|
|
55
63
|
{table.rows.map((row) =>
|
|
56
|
-
<TableRow
|
|
64
|
+
<TableRow
|
|
65
|
+
key={row.id}
|
|
66
|
+
className={row.showPath ? "cursor-pointer" : ""}
|
|
67
|
+
onClick={(e) => handleRowClick(e, row.showPath)}>
|
|
57
68
|
{row.cells.map((cell) =>
|
|
58
69
|
<TableCell key={cell.attribute}>
|
|
59
|
-
|
|
70
|
+
{cell.showPath ? (
|
|
71
|
+
<a href={cell.showPath} data-sg-visit className="hover:underline">
|
|
72
|
+
<FieldRenderer mode="index" {...cell} />
|
|
73
|
+
</a>
|
|
74
|
+
) : (
|
|
75
|
+
<FieldRenderer mode="index" {...cell} />
|
|
76
|
+
)}
|
|
60
77
|
</TableCell>
|
|
61
78
|
)}
|
|
62
79
|
<TableCell>
|
|
@@ -69,7 +69,13 @@ export default function AdminShow() {
|
|
|
69
69
|
{attr.label}
|
|
70
70
|
</dt>
|
|
71
71
|
<dd className="col-span-2 text-sm">
|
|
72
|
-
|
|
72
|
+
{attr.showPath ? (
|
|
73
|
+
<a href={attr.showPath} data-sg-visit className="hover:underline">
|
|
74
|
+
<FieldRenderer mode="show" {...attr} />
|
|
75
|
+
</a>
|
|
76
|
+
) : (
|
|
77
|
+
<FieldRenderer mode="show" {...attr} />
|
|
78
|
+
)}
|
|
73
79
|
</dd>
|
|
74
80
|
</div>
|
|
75
81
|
)}
|
|
@@ -63,7 +63,9 @@ module Terrazzo
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def collection_includes
|
|
66
|
+
collection_attr_set = Set.new(collection_attributes)
|
|
66
67
|
attribute_types.each_with_object([]) do |(attr, type), includes|
|
|
68
|
+
next unless collection_attr_set.include?(attr)
|
|
67
69
|
next unless type.eager_load?
|
|
68
70
|
next unless self.class.model.reflect_on_association(attr)
|
|
69
71
|
includes << attr
|
data/lib/terrazzo/engine.rb
CHANGED
|
@@ -17,5 +17,11 @@ module Terrazzo
|
|
|
17
17
|
app.config.superglue = ActiveSupport::OrderedOptions.new unless app.config.respond_to?(:superglue)
|
|
18
18
|
app.config.superglue.auto_include = false
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
initializer "terrazzo.uses_superglue" do
|
|
22
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
23
|
+
extend Terrazzo::UsesSuperglue
|
|
24
|
+
end
|
|
25
|
+
end
|
|
20
26
|
end
|
|
21
27
|
end
|
data/lib/terrazzo/field/base.rb
CHANGED
|
@@ -19,11 +19,11 @@ module Terrazzo
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def searchable?
|
|
22
|
-
deferred_class.searchable?
|
|
22
|
+
options.key?(:searchable) ? options[:searchable] : deferred_class.searchable?
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def sortable?
|
|
26
|
-
deferred_class.sortable?
|
|
26
|
+
options.key?(:sortable) ? options[:sortable] : deferred_class.sortable?
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def eager_load?
|
data/lib/terrazzo/field/email.rb
CHANGED
|
@@ -42,22 +42,17 @@ module Terrazzo
|
|
|
42
42
|
|
|
43
43
|
def serialize_show_value
|
|
44
44
|
limit = options.fetch(:limit, 5)
|
|
45
|
-
|
|
45
|
+
all_records = data.to_a
|
|
46
|
+
total = all_records.size
|
|
46
47
|
col_attrs = options[:collection_attributes]
|
|
47
48
|
|
|
48
|
-
records = if limit && limit > 0
|
|
49
|
-
data.respond_to?(:limit) ? data.limit(limit) : data.first(limit)
|
|
50
|
-
else
|
|
51
|
-
data
|
|
52
|
-
end
|
|
53
|
-
|
|
54
49
|
if col_attrs
|
|
55
|
-
serialize_with_collection_attributes(
|
|
50
|
+
serialize_with_collection_attributes(all_records, col_attrs, total, limit)
|
|
56
51
|
else
|
|
57
52
|
{
|
|
58
|
-
items:
|
|
53
|
+
items: all_records.map { |r| { id: r.id, display: display_name(r) } },
|
|
59
54
|
total: total,
|
|
60
|
-
|
|
55
|
+
initialLimit: limit
|
|
61
56
|
}
|
|
62
57
|
end
|
|
63
58
|
end
|
|
@@ -85,7 +80,7 @@ module Terrazzo
|
|
|
85
80
|
headers: headers,
|
|
86
81
|
items: items,
|
|
87
82
|
total: total,
|
|
88
|
-
|
|
83
|
+
initialLimit: limit
|
|
89
84
|
}
|
|
90
85
|
end
|
|
91
86
|
|
data/lib/terrazzo/field/text.rb
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Terrazzo
|
|
4
|
+
module UsesSuperglue
|
|
5
|
+
# Prepended to fix a template shadowing problem.
|
|
6
|
+
#
|
|
7
|
+
# When a controller inherits a long view prefix chain (e.g. from Devise or
|
|
8
|
+
# other engines), Superglue's _render_template calls template_exists? with
|
|
9
|
+
# all those prefixes and may find a gem-supplied HTML template, causing it
|
|
10
|
+
# to render the unstyled ERB instead of the React page. This override
|
|
11
|
+
# restricts the template existence check to the app's own view path only.
|
|
12
|
+
module TemplateLookupOverride
|
|
13
|
+
def _render_template(options = {})
|
|
14
|
+
if @_capture_options_before_render
|
|
15
|
+
@_capture_options_before_render = false
|
|
16
|
+
@_render_options = options
|
|
17
|
+
_ensure_react_page!(options[:template], options[:prefixes])
|
|
18
|
+
|
|
19
|
+
app_views = Rails.root.join("app/views").to_s
|
|
20
|
+
app_resolver = view_paths.to_a.find { |vp| vp.to_s == app_views && !vp.is_a?(Superglue::Resolver) }
|
|
21
|
+
prefixes = Array(options[:prefixes]).compact
|
|
22
|
+
html_template_exist = app_resolver&.find_all(options[:template], prefixes, false, { formats: [:html], locale: [], handlers: [], variants: [] }, nil, [])&.any?
|
|
23
|
+
if !html_template_exist
|
|
24
|
+
super(options.merge(template: _superglue_template, prefixes: []))
|
|
25
|
+
else
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
else
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def uses_superglue
|
|
35
|
+
include Superglue::Controller
|
|
36
|
+
prepend_view_path(Superglue::Resolver.new(Rails.root.join("app/views")))
|
|
37
|
+
before_action :use_jsx_rendering_defaults
|
|
38
|
+
prepend TemplateLookupOverride
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/terrazzo/version.rb
CHANGED
data/lib/terrazzo.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Terrazzo
|
|
|
13
13
|
autoload :Filter, "terrazzo/filter"
|
|
14
14
|
autoload :Namespace, "terrazzo/namespace"
|
|
15
15
|
autoload :GeneratorHelpers, "terrazzo/generator_helpers"
|
|
16
|
+
autoload :UsesSuperglue, "terrazzo/uses_superglue"
|
|
16
17
|
|
|
17
18
|
module Field
|
|
18
19
|
autoload :Base, "terrazzo/field/base"
|
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.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Terrazzo Contributors
|
|
@@ -132,6 +132,7 @@ files:
|
|
|
132
132
|
- lib/generators/terrazzo/field/templates/ShowField.jsx.erb
|
|
133
133
|
- lib/generators/terrazzo/field/templates/field.rb.erb
|
|
134
134
|
- lib/generators/terrazzo/install/install_generator.rb
|
|
135
|
+
- lib/generators/terrazzo/install/templates/admin.css
|
|
135
136
|
- lib/generators/terrazzo/install/templates/application.js.erb
|
|
136
137
|
- lib/generators/terrazzo/install/templates/application.json.props
|
|
137
138
|
- lib/generators/terrazzo/install/templates/application.json.props.erb
|
|
@@ -271,6 +272,7 @@ files:
|
|
|
271
272
|
- lib/terrazzo/page/show.rb
|
|
272
273
|
- lib/terrazzo/resource_resolver.rb
|
|
273
274
|
- lib/terrazzo/search.rb
|
|
275
|
+
- lib/terrazzo/uses_superglue.rb
|
|
274
276
|
- lib/terrazzo/version.rb
|
|
275
277
|
- terrazzo.gemspec
|
|
276
278
|
homepage: https://github.com/wengzilla/terrazzo
|