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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ff3b38b0cdec57570baa990aef1998197eba301cfa0c297d0619ff7d4003374
4
- data.tar.gz: f0556e47ac6c647db017b2ebb0167a5f9585a5d3c9353bab740131fbd717b02b
3
+ metadata.gz: b55338dd365f9ad475e3892c34a4a5ca706625b50bd331dd6793bbd756ec75e7
4
+ data.tar.gz: a5193fda7a10ca72103816e0f47a58ba7facf375003a4af22efa0385914378b2
5
5
  SHA512:
6
- metadata.gz: b46cb63e1fe4754a6938fec445f76aa90eba36f55c3fde38f08713db3e325f504b3fe985816625a07e5a8b77e7fe6d820c62f262dd20d443deaa6566111c75db
7
- data.tar.gz: '0974a53199cdbdb29b8e90096429ee853df3bbeb5414cb60d1ef2dceca0c6356a7533921a3ab383a19b04bc408e08658e8f5e4d58a55f7dbfb5caa06454ad39f'
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.attribute field.attribute.to_s
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.attribute field.attribute.to_s
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.application.eager_load! if defined?(Rails)
79
- ApplicationRecord.descendants.reject(&:abstract_class?)
80
- rescue
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 function buildVisitAndRemote(ref, store, pageToPageMapping) {
4
- const appVisit = visit(ref, store, pageToPageMapping)
5
- const appRemote = remote(ref, store, pageToPageMapping)
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
- route_content = <<~RUBY.indent(2)
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 route_content.strip
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.application.eager_load! if defined?(Rails)
32
- ApplicationRecord.descendants.reject(&:abstract_class?)
33
- rescue
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
@@ -21,3 +21,5 @@ export function Layout({ navigation, title, actions, children }) {
21
21
  </SidebarProvider>);
22
22
 
23
23
  }
24
+
25
+ export default Layout;
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { ArrowUp, ArrowDown, ArrowUpDown } from "lucide-react";
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
- <ArrowUp className="h-4 w-4" /> :
20
+ <ChevronUp className="h-4 w-4" /> :
21
21
  sortDirection === "desc" ?
22
- <ArrowDown className="h-4 w-4" /> :
22
+ <ChevronDown className="h-4 w-4" /> :
23
23
 
24
- <ArrowUpDown className="h-4 w-4" />
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, hasMore } = value;
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
- {items.map((item) =>
37
- <TableRow key={item.id}>
38
- {item.columns.map((col) =>
39
- <TableCell key={col.attribute}>
40
- <FieldRenderer mode="index" {...col} />
41
- </TableCell>
42
- )}
43
- </TableRow>
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
- <p className="text-sm text-muted-foreground mt-2">
50
- Showing {items.length} of {total}
51
- </p>
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
- <ul className="list-disc pl-5 space-y-1">
61
- {items.map((item) =>
62
- <li key={item.id}>{item.display}</li>
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
- </ul>
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 key={row.id}>
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
- <FieldRenderer mode="index" {...cell} />
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
- <FieldRenderer mode="show" {...attr} />
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
@@ -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
@@ -107,7 +107,7 @@ module Terrazzo
107
107
  private
108
108
 
109
109
  def resolve_data
110
- resource.public_send(attribute)
110
+ resource.try(attribute)
111
111
  end
112
112
  end
113
113
  end
@@ -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?
@@ -4,12 +4,6 @@ module Terrazzo
4
4
  def serialize_value(_mode)
5
5
  data
6
6
  end
7
-
8
- class << self
9
- def searchable?
10
- true
11
- end
12
- end
13
7
  end
14
8
  end
15
9
  end
@@ -42,22 +42,17 @@ module Terrazzo
42
42
 
43
43
  def serialize_show_value
44
44
  limit = options.fetch(:limit, 5)
45
- total = data.size
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(records, col_attrs, total, limit)
50
+ serialize_with_collection_attributes(all_records, col_attrs, total, limit)
56
51
  else
57
52
  {
58
- items: records.map { |r| { id: r.id, display: display_name(r) } },
53
+ items: all_records.map { |r| { id: r.id, display: display_name(r) } },
59
54
  total: total,
60
- hasMore: limit && limit > 0 ? total > limit : false
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
- hasMore: limit && limit > 0 ? total > limit : false
83
+ initialLimit: limit
89
84
  }
90
85
  end
91
86
 
@@ -14,10 +14,6 @@ module Terrazzo
14
14
  end
15
15
 
16
16
  class << self
17
- def searchable?
18
- true
19
- end
20
-
21
17
  def default_options
22
18
  { truncate: 50 }
23
19
  end
@@ -14,10 +14,6 @@ module Terrazzo
14
14
  end
15
15
 
16
16
  class << self
17
- def searchable?
18
- true
19
- end
20
-
21
17
  def default_options
22
18
  { truncate: 100 }
23
19
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Terrazzo
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.2"
3
3
  end
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.1.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