terrazzo 0.6.0 → 0.7.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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -0
  3. data/Rakefile +1 -0
  4. data/app/controllers/terrazzo/application_controller.rb +63 -14
  5. data/app/helpers/terrazzo/collection_actions_helper.rb +38 -13
  6. data/app/helpers/terrazzo/resource_paths_helper.rb +55 -0
  7. data/app/views/terrazzo/application/_edit_base.json.props +7 -4
  8. data/app/views/terrazzo/application/_index_base.json.props +83 -7
  9. data/app/views/terrazzo/application/_navigation.json.props +7 -4
  10. data/app/views/terrazzo/application/_new_base.json.props +6 -3
  11. data/app/views/terrazzo/application/_show_base.json.props +7 -5
  12. data/lib/generators/terrazzo/dashboard/dashboard_generator.rb +48 -24
  13. data/lib/generators/terrazzo/dashboard/templates/controller.rb.erb +8 -4
  14. data/lib/generators/terrazzo/dashboard/templates/dashboard.rb.erb +33 -25
  15. data/lib/generators/terrazzo/eject/eject_generator.rb +223 -113
  16. data/lib/generators/terrazzo/field/field_generator.rb +51 -7
  17. data/lib/generators/terrazzo/field/templates/FormField.jsx.erb +2 -3
  18. data/lib/generators/terrazzo/install/install_generator.rb +641 -4
  19. data/lib/generators/terrazzo/install/templates/admin.css +2 -0
  20. data/lib/generators/terrazzo/install/templates/application.js.erb +3 -0
  21. data/lib/generators/terrazzo/install/templates/components.json.erb +17 -0
  22. data/lib/generators/terrazzo/install/templates/custom_page_mapping.js.erb +5 -0
  23. data/lib/generators/terrazzo/install/templates/generated_page_mapping.js.erb +10 -0
  24. data/lib/generators/terrazzo/install/templates/jsconfig.json +8 -0
  25. data/lib/generators/terrazzo/install/templates/page_to_page_mapping.js.erb +9 -1
  26. data/lib/generators/terrazzo/install/templates/superglue.html.erb.erb +2 -2
  27. data/lib/generators/terrazzo/routes/routes_generator.rb +55 -20
  28. data/lib/generators/terrazzo/views/edit_generator.rb +16 -10
  29. data/lib/generators/terrazzo/views/eject_compatibility.rb +19 -0
  30. data/lib/generators/terrazzo/views/field_generator.rb +18 -6
  31. data/lib/generators/terrazzo/views/generated_defaults_helper.rb +110 -0
  32. data/lib/generators/terrazzo/views/index_generator.rb +9 -8
  33. data/lib/generators/terrazzo/views/layout_generator.rb +4 -3
  34. data/lib/generators/terrazzo/views/navigation_generator.rb +5 -1
  35. data/lib/generators/terrazzo/views/new_generator.rb +16 -10
  36. data/lib/generators/terrazzo/views/page_mapping_helper.rb +131 -8
  37. data/lib/generators/terrazzo/views/show_generator.rb +7 -6
  38. data/lib/generators/terrazzo/views/templates/components/CollectionFilters.jsx +25 -0
  39. data/lib/generators/terrazzo/views/templates/components/CollectionItemActions.jsx +68 -0
  40. data/lib/generators/terrazzo/views/templates/components/CollectionToolbarActions.jsx +60 -0
  41. data/lib/generators/terrazzo/views/templates/components/HasManyPagination.jsx +36 -0
  42. data/lib/generators/terrazzo/views/templates/components/Layout.jsx +9 -4
  43. data/lib/generators/terrazzo/views/templates/components/Pagination.jsx +32 -24
  44. data/lib/generators/terrazzo/views/templates/components/ResourceTable.jsx +172 -0
  45. data/lib/generators/terrazzo/views/templates/components/SearchBar.jsx +14 -6
  46. data/lib/generators/terrazzo/views/templates/components/SortableHeader.jsx +7 -5
  47. data/lib/generators/terrazzo/views/templates/components/app-sidebar.jsx +1 -1
  48. data/lib/generators/terrazzo/views/templates/components/site-header.jsx +2 -2
  49. data/lib/generators/terrazzo/views/templates/fields/asset/FormField.jsx +15 -0
  50. data/lib/generators/terrazzo/views/templates/fields/asset/IndexField.jsx +5 -0
  51. data/lib/generators/terrazzo/views/templates/fields/asset/ShowField.jsx +13 -0
  52. data/lib/generators/terrazzo/views/templates/fields/belongs_to/FormField.jsx +1 -1
  53. data/lib/generators/terrazzo/views/templates/fields/boolean/FormField.jsx +1 -1
  54. data/lib/generators/terrazzo/views/templates/fields/boolean/IndexField.jsx +1 -1
  55. data/lib/generators/terrazzo/views/templates/fields/has_many/FormField.jsx +3 -3
  56. data/lib/generators/terrazzo/views/templates/fields/has_many/IndexField.jsx +4 -3
  57. data/lib/generators/terrazzo/views/templates/fields/has_many/ShowField.jsx +21 -35
  58. data/lib/generators/terrazzo/views/templates/fields/has_one/FormField.jsx +1 -1
  59. data/lib/generators/terrazzo/views/templates/fields/hstore/FormField.jsx +5 -5
  60. data/lib/generators/terrazzo/views/templates/fields/hstore/IndexField.jsx +0 -2
  61. data/lib/generators/terrazzo/views/templates/fields/hstore/ShowField.jsx +1 -1
  62. data/lib/generators/terrazzo/views/templates/fields/number/IndexField.jsx +1 -1
  63. data/lib/generators/terrazzo/views/templates/fields/number/ShowField.jsx +1 -1
  64. data/lib/generators/terrazzo/views/templates/fields/polymorphic/FormField.jsx +3 -2
  65. data/lib/generators/terrazzo/views/templates/fields/rich_text/FormField.jsx +2 -2
  66. data/lib/generators/terrazzo/views/templates/fields/select/FormField.jsx +1 -1
  67. data/lib/generators/terrazzo/views/templates/fields/select/IndexField.jsx +1 -1
  68. data/lib/generators/terrazzo/views/templates/fields/shared/TextInputFormField.jsx +2 -2
  69. data/lib/generators/terrazzo/views/templates/fields/text/FormField.jsx +2 -2
  70. data/lib/generators/terrazzo/views/templates/pages/_collection.jsx +5 -3
  71. data/lib/generators/terrazzo/views/templates/pages/_form.jsx +37 -30
  72. data/lib/generators/terrazzo/views/templates/pages/_navigation.json.props +14 -4
  73. data/lib/generators/terrazzo/views/templates/pages/edit.jsx +6 -4
  74. data/lib/generators/terrazzo/views/templates/pages/index.jsx +20 -8
  75. data/lib/generators/terrazzo/views/templates/pages/new.jsx +8 -4
  76. data/lib/generators/terrazzo/views/templates/pages/show.jsx +30 -32
  77. data/lib/generators/terrazzo/views/views_generator.rb +9 -5
  78. data/lib/terrazzo/base_dashboard.rb +167 -5
  79. data/lib/terrazzo/csv_export.rb +38 -0
  80. data/lib/terrazzo/field/associative.rb +16 -4
  81. data/lib/terrazzo/field/base.rb +4 -0
  82. data/lib/terrazzo/field/belongs_to.rb +1 -2
  83. data/lib/terrazzo/field/deferred.rb +4 -0
  84. data/lib/terrazzo/field/has_many.rb +79 -11
  85. data/lib/terrazzo/field/password.rb +6 -0
  86. data/lib/terrazzo/field/rich_text.rb +13 -0
  87. data/lib/terrazzo/generator_helpers.rb +1 -1
  88. data/lib/terrazzo/has_many_pagination.rb +9 -1
  89. data/lib/terrazzo/namespace/resource.rb +30 -0
  90. data/lib/terrazzo/namespace.rb +26 -1
  91. data/lib/terrazzo/search.rb +29 -9
  92. data/lib/terrazzo/version.rb +1 -1
  93. data/lib/terrazzo.rb +1 -0
  94. data/terrazzo.gemspec +11 -1
  95. metadata +40 -6
  96. data/lib/generators/terrazzo/install/templates/application.json.props.erb +0 -17
  97. data/lib/generators/terrazzo/views/templates/fields/FieldRenderer.jsx +0 -103
  98. data/lib/generators/terrazzo/views/templates/fields/index.js +0 -81
@@ -2,26 +2,149 @@ module Terrazzo
2
2
  module Generators
3
3
  module Views
4
4
  module PageMappingHelper
5
+ GENERATED_IMPORT_MARKER = "// Terrazzo generated page imports end"
6
+ GENERATED_MAPPING_MARKER = " // Terrazzo generated page mappings end"
7
+
5
8
  private
6
9
 
10
+ def ensure_page_barrels
11
+ ensure_barrel("app/views/#{namespace_name}/fields/index.js", 'export * from "terrazzo/fields";')
12
+ ensure_barrel("app/views/#{namespace_name}/components/index.js", 'export * from "terrazzo/components";')
13
+ ensure_barrel("app/views/#{namespace_name}/components/ui/index.js", 'export * from "terrazzo/ui";')
14
+ end
15
+
7
16
  def register_page_mapping(action)
8
- mapping_path = File.join(destination_root, "app/javascript/#{namespace_name}/page_to_page_mapping.js")
9
- return unless File.exist?(mapping_path)
17
+ mapping_path = "app/javascript/#{namespace_name}/generated_page_mapping.js"
18
+ mapping_file = File.join(destination_root, mapping_path)
19
+
20
+ unless File.exist?(mapping_file)
21
+ create_file mapping_path, <<~JS
22
+ // Resource-specific page mappings generated by Terrazzo.
23
+ // Add manual custom pages to custom_page_mapping.js instead.
10
24
 
11
- component_name = "#{resource.gsub('::', '')}#{action.capitalize}"
25
+ // Terrazzo generated page imports start
26
+ // Terrazzo generated page imports end
27
+
28
+ export const generatedPageMapping = {
29
+ // Terrazzo generated page mappings start
30
+ // Terrazzo generated page mappings end
31
+ }
32
+ JS
33
+ end
34
+
35
+ component_name = generated_page_component_name(action)
12
36
  import_path = "../../views/#{namespace_name}/#{resource_path}/#{action}"
13
37
  key = "'#{namespace_name}/#{resource_path}/#{action}'"
14
38
 
15
- content = File.read(mapping_path)
39
+ content = File.read(mapping_file)
40
+ ensure_generated_mapping_loaded
16
41
  return if content.include?(key)
17
42
 
18
- # Add import before the pages object declaration
19
43
  import_line = "import #{component_name} from \"#{import_path}\";\n"
20
- inject_into_file mapping_path, import_line, before: "\nconst pages"
44
+ insert_generated_import(mapping_path, content, import_line, component_name)
21
45
 
22
- # Add mapping entry inside the pages object (before its closing brace)
23
46
  entry_line = " #{key}: #{component_name},\n"
24
- inject_into_file mapping_path, entry_line, before: "}\n\n//"
47
+ insert_generated_mapping_entry(mapping_path, File.read(mapping_file), entry_line, key)
48
+ end
49
+
50
+ def ensure_generated_mapping_loaded
51
+ main_mapping_path = "app/javascript/#{namespace_name}/page_to_page_mapping.js"
52
+ main_mapping_file = File.join(destination_root, main_mapping_path)
53
+ return unless File.exist?(main_mapping_file)
54
+
55
+ content = File.read(main_mapping_file)
56
+
57
+ unless imports_generated_mapping?(content)
58
+ import_line = %(import { generatedPageMapping } from "./generated_page_mapping";\n)
59
+ if content.include?(%(import { customPageMapping } from "./custom_page_mapping";))
60
+ inject_into_file main_mapping_path, import_line,
61
+ before: %(import { customPageMapping } from "./custom_page_mapping";)
62
+ else
63
+ prepend_to_file main_mapping_path, import_line
64
+ end
65
+ content = File.read(main_mapping_file)
66
+ end
67
+
68
+ return if generated_mapping_used_in_code?(content)
69
+
70
+ generated_spread = " ...generatedPageMapping,\n"
71
+ if content.include?(" ...customPageMapping,")
72
+ inject_into_file main_mapping_path, generated_spread, before: " ...customPageMapping,"
73
+ elsif content.include?("...customPageMapping")
74
+ gsub_file main_mapping_path, "...customPageMapping", "...generatedPageMapping, ...customPageMapping"
75
+ elsif content.include?("const pages = {\n")
76
+ inject_into_file main_mapping_path, generated_spread, after: "const pages = {\n"
77
+ else
78
+ raise Thor::Error,
79
+ "#{main_mapping_path} imports generated_page_mapping.js but Terrazzo could not find the pages object to merge it automatically."
80
+ end
81
+ end
82
+
83
+ def imports_generated_mapping?(content)
84
+ content.match?(/^import\s+\{\s*generatedPageMapping\s*\}\s+from\s+["']\.\/generated_page_mapping["'];?\s*$/)
85
+ end
86
+
87
+ def generated_mapping_used_in_code?(content)
88
+ executable_content = content
89
+ .gsub(/^import\s+.*\n/, "")
90
+ .gsub(%r{//.*$}, "")
91
+ .gsub(%r{/\*.*?\*/}m, "")
92
+
93
+ executable_content.match?(/\bgeneratedPageMapping\b/)
94
+ end
95
+
96
+ def resource_class_name
97
+ resource.to_s.camelize
98
+ end
99
+
100
+ def resource_path
101
+ resource_class_name.underscore.pluralize
102
+ end
103
+
104
+ def generated_page_component_name(action)
105
+ resource_class_name.split("::").map(&:camelize).join("__") + action.camelize
106
+ end
107
+
108
+ def insert_generated_import(mapping_path, content, import_line, component_name)
109
+ if content.include?(GENERATED_IMPORT_MARKER)
110
+ inject_into_file mapping_path, import_line, before: GENERATED_IMPORT_MARKER
111
+ elsif content.include?("export const generatedPageMapping")
112
+ prepend_to_file mapping_path, import_line
113
+ say_status :warning,
114
+ "#{mapping_path} is missing #{GENERATED_IMPORT_MARKER.inspect}; prepended #{component_name} import.",
115
+ :yellow
116
+ else
117
+ raise Thor::Error,
118
+ "#{mapping_path} must export generatedPageMapping so Terrazzo can register resource-specific pages."
119
+ end
120
+ end
121
+
122
+ def insert_generated_mapping_entry(mapping_path, content, entry_line, key)
123
+ if content.include?(GENERATED_MAPPING_MARKER)
124
+ inject_into_file mapping_path, entry_line, before: GENERATED_MAPPING_MARKER
125
+ elsif content.include?("export const generatedPageMapping = {\n")
126
+ inject_into_file mapping_path, entry_line, after: "export const generatedPageMapping = {\n"
127
+ say_status :warning,
128
+ "#{mapping_path} is missing #{GENERATED_MAPPING_MARKER.strip.inspect}; inserted #{key} at the start of generatedPageMapping.",
129
+ :yellow
130
+ else
131
+ raise Thor::Error,
132
+ "#{mapping_path} must export generatedPageMapping so Terrazzo can register #{key}."
133
+ end
134
+ end
135
+
136
+ def ensure_barrel(barrel_path, package_export)
137
+ barrel_file = File.join(destination_root, barrel_path)
138
+
139
+ unless File.exist?(barrel_file)
140
+ create_file barrel_path, "#{package_export}\n"
141
+ return
142
+ end
143
+
144
+ content = File.read(barrel_file)
145
+ return if content.include?(package_export)
146
+
147
+ prepend_to_file barrel_path, "#{package_export}\n"
25
148
  end
26
149
  end
27
150
  end
@@ -1,10 +1,12 @@
1
1
  require "rails/generators"
2
+ require_relative "generated_defaults_helper"
2
3
  require_relative "page_mapping_helper"
3
4
 
4
5
  module Terrazzo
5
6
  module Generators
6
7
  module Views
7
8
  class ShowGenerator < Rails::Generators::Base
9
+ include GeneratedDefaultsHelper
8
10
  include PageMappingHelper
9
11
 
10
12
  source_root File.expand_path("templates", __dir__)
@@ -16,12 +18,15 @@ module Terrazzo
16
18
  desc: "Admin namespace"
17
19
 
18
20
  def copy_show_template
21
+ ensure_page_barrels
22
+
19
23
  if resource.present?
20
24
  eject_json_props
21
- copy_file "pages/show.jsx", "app/views/#{namespace_name}/#{resource_path}/show.jsx"
25
+ copy_resource_page_file "pages/show.jsx", "app/views/#{namespace_name}/#{resource_path}/show.jsx"
22
26
  register_page_mapping("show")
23
27
  else
24
- copy_file "pages/show.jsx", "app/views/#{namespace_name}/application/show.jsx"
28
+ copy_file_over_generated "pages/show.jsx", "app/views/#{namespace_name}/application/show.jsx",
29
+ generated_content: generated_page_stub("show")
25
30
  end
26
31
  end
27
32
 
@@ -35,10 +40,6 @@ module Terrazzo
35
40
  RUBY
36
41
  end
37
42
 
38
- def resource_path
39
- resource.underscore.pluralize
40
- end
41
-
42
43
  def namespace_name
43
44
  options[:namespace]
44
45
  end
@@ -0,0 +1,25 @@
1
+ import React from "react";
2
+
3
+ import { Button } from "./ui";
4
+
5
+ export function CollectionFilters({ active, allUrl, options }) {
6
+ if (!options || options.length === 0) return null;
7
+
8
+ return (
9
+ <div className="flex flex-wrap items-center gap-2">
10
+ <Button asChild variant={active ? "outline" : "secondary"} size="sm">
11
+ <a href={allUrl} data-sg-visit>All</a>
12
+ </Button>
13
+ {options.map((option) => (
14
+ <Button
15
+ key={option.value}
16
+ asChild
17
+ variant={option.active ? "secondary" : "outline"}
18
+ size="sm"
19
+ >
20
+ <a href={option.url} data-sg-visit>{option.label}</a>
21
+ </Button>
22
+ ))}
23
+ </div>
24
+ );
25
+ }
@@ -0,0 +1,68 @@
1
+ import React from "react";
2
+ import { MoreHorizontal } from "lucide-react";
3
+
4
+ import {
5
+ Button,
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuTrigger,
10
+ } from "./ui";
11
+ import { csrfToken } from "terrazzo";
12
+
13
+ export function CollectionItemActions({ actions }) {
14
+ if (!actions || actions.length === 0) return null;
15
+
16
+ return (
17
+ <DropdownMenu>
18
+ <DropdownMenuTrigger asChild>
19
+ <Button variant="ghost" size="icon" className="h-8 w-8" aria-label="Open row actions">
20
+ <MoreHorizontal className="h-4 w-4" />
21
+ </Button>
22
+ </DropdownMenuTrigger>
23
+ <DropdownMenuContent align="end">
24
+ {actions.map((action, index) => {
25
+ const method = String(action.method || "get").toLowerCase();
26
+
27
+ if (method !== "get") {
28
+ const isDestructive = method === "delete";
29
+ return (
30
+ <DropdownMenuItem key={index} asChild>
31
+ <form
32
+ action={action.url}
33
+ method="post"
34
+ {...(action.sg_visit !== false && { "data-sg-visit": true })}
35
+ onSubmit={(e) => {
36
+ if (action.confirm && !window.confirm(action.confirm)) {
37
+ e.preventDefault();
38
+ }
39
+ }}
40
+ >
41
+ {method !== "post" && (
42
+ <input type="hidden" name="_method" value={method} />
43
+ )}
44
+ <input
45
+ type="hidden"
46
+ name="authenticity_token"
47
+ value={csrfToken()}
48
+ />
49
+ <button type="submit" className={`w-full text-left ${isDestructive ? "text-destructive" : ""}`}>
50
+ {action.label}
51
+ </button>
52
+ </form>
53
+ </DropdownMenuItem>
54
+ );
55
+ }
56
+
57
+ return (
58
+ <DropdownMenuItem key={index} asChild>
59
+ <a href={action.url} {...(action.sg_visit !== false && { "data-sg-visit": true })}>
60
+ {action.label}
61
+ </a>
62
+ </DropdownMenuItem>
63
+ );
64
+ })}
65
+ </DropdownMenuContent>
66
+ </DropdownMenu>
67
+ );
68
+ }
@@ -0,0 +1,60 @@
1
+ import React from "react";
2
+
3
+ import { Button } from "./ui";
4
+ import { csrfToken } from "terrazzo";
5
+
6
+ export function CollectionToolbarActions({ actions, selectedIds, disabled = false }) {
7
+ if (!actions || actions.length === 0) return null;
8
+ const selectedValues = Array.isArray(selectedIds) ? selectedIds : null;
9
+
10
+ return actions.map((action, index) => {
11
+ const method = String(action.method || "get").toLowerCase();
12
+ const rendersSelectedIds = selectedValues !== null;
13
+
14
+ if (method !== "get" || rendersSelectedIds) {
15
+ const isDestructive = method === "delete";
16
+ const variant = action.variant || (isDestructive ? "destructive" : "outline");
17
+ const formMethod = method === "get" ? "get" : "post";
18
+ const selectedParamName = action.paramName || action.param_name || "ids[]";
19
+
20
+ return (
21
+ <form
22
+ key={index}
23
+ action={action.url}
24
+ method={formMethod}
25
+ {...(action.sg_visit !== false && { "data-sg-visit": true })}
26
+ onSubmit={(e) => {
27
+ if (action.confirm && !window.confirm(action.confirm)) {
28
+ e.preventDefault();
29
+ }
30
+ }}
31
+ >
32
+ {method !== "get" && method !== "post" && (
33
+ <input type="hidden" name="_method" value={method} />
34
+ )}
35
+ {method !== "get" && (
36
+ <input
37
+ type="hidden"
38
+ name="authenticity_token"
39
+ value={csrfToken()}
40
+ />
41
+ )}
42
+ {selectedValues?.map((id) => (
43
+ <input key={id} type="hidden" name={selectedParamName} value={id} />
44
+ ))}
45
+ <Button type="submit" variant={variant} size="sm" disabled={disabled}>
46
+ {action.label}
47
+ </Button>
48
+ </form>
49
+ );
50
+ }
51
+
52
+ return (
53
+ <Button key={index} asChild variant={action.variant || "outline"} size="sm">
54
+ <a href={action.url} {...(action.sg_visit !== false && { "data-sg-visit": true })}>
55
+ {action.label}
56
+ </a>
57
+ </Button>
58
+ );
59
+ });
60
+ }
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { ChevronLeft, ChevronRight } from "lucide-react";
3
+
4
+ import { Button } from "./ui";
5
+
6
+ export function HasManyPagination({ currentPage, totalPages, total, nextPagePath, prevPagePath }) {
7
+ if (totalPages <= 1) return null;
8
+
9
+ const updateUrl = (path) => () => {
10
+ const url = new URL(path, window.location.origin);
11
+ url.searchParams.delete("props_at");
12
+ window.history.replaceState(window.history.state, "", url.pathname + url.search);
13
+ };
14
+
15
+ return (
16
+ <div className="flex items-center justify-between gap-2 pt-2 text-sm text-muted-foreground">
17
+ <span>Page {currentPage} of {totalPages} &middot; {total} total</span>
18
+ <div className="flex items-center gap-1">
19
+ {prevPagePath ? (
20
+ <Button asChild variant="outline" size="sm">
21
+ <a href={prevPagePath} data-sg-remote onClick={updateUrl(prevPagePath)}><ChevronLeft className="h-4 w-4" /> Prev</a>
22
+ </Button>
23
+ ) : (
24
+ <Button variant="outline" size="sm" disabled><ChevronLeft className="h-4 w-4" /> Prev</Button>
25
+ )}
26
+ {nextPagePath ? (
27
+ <Button asChild variant="outline" size="sm">
28
+ <a href={nextPagePath} data-sg-remote onClick={updateUrl(nextPagePath)}>Next <ChevronRight className="h-4 w-4" /></a>
29
+ </Button>
30
+ ) : (
31
+ <Button variant="outline" size="sm" disabled>Next <ChevronRight className="h-4 w-4" /></Button>
32
+ )}
33
+ </div>
34
+ </div>
35
+ );
36
+ }
@@ -1,11 +1,16 @@
1
1
  import React from "react";
2
2
 
3
- import { AppSidebar } from "./app-sidebar";
4
- import { SiteHeader } from "./site-header";
5
- import { FlashMessages } from "./FlashMessages";
6
- import { SidebarProvider, SidebarInset } from "terrazzo/ui";
3
+ import { getComponent } from "terrazzo";
4
+ import { AppSidebar as DefaultAppSidebar } from "./app-sidebar";
5
+ import { SiteHeader as DefaultSiteHeader } from "./site-header";
6
+ import { FlashMessages as DefaultFlashMessages } from "./FlashMessages";
7
+ import { SidebarProvider, SidebarInset } from "./ui";
7
8
 
8
9
  export function Layout({ navigation, title, actions, children }) {
10
+ const AppSidebar = getComponent("AppSidebar") || DefaultAppSidebar;
11
+ const SiteHeader = getComponent("SiteHeader") || DefaultSiteHeader;
12
+ const FlashMessages = getComponent("FlashMessages") || DefaultFlashMessages;
13
+
9
14
  return (
10
15
  <SidebarProvider>
11
16
  <AppSidebar variant="inset" navigation={navigation} />
@@ -1,14 +1,14 @@
1
1
  import React, { useContext } from "react";
2
2
  import { NavigationContext } from "@thoughtbot/superglue";
3
3
 
4
- import { Field, FieldLabel } from "terrazzo/ui";
4
+ import { Field, FieldLabel } from "./ui";
5
5
  import {
6
6
  Pagination as PaginationRoot,
7
7
  PaginationContent,
8
8
  PaginationItem,
9
9
  PaginationNext,
10
10
  PaginationPrevious,
11
- } from "terrazzo/ui";
11
+ } from "./ui";
12
12
  import {
13
13
  Select,
14
14
  SelectContent,
@@ -16,48 +16,56 @@ import {
16
16
  SelectItem,
17
17
  SelectTrigger,
18
18
  SelectValue,
19
- } from "terrazzo/ui";
19
+ } from "./ui";
20
20
 
21
21
  export function Pagination({ currentPage, totalPages, totalCount, perPage, nextPagePath, prevPagePath }) {
22
- const { remote, pageKey } = useContext(NavigationContext);
22
+ const { visit } = useContext(NavigationContext);
23
+ const page = Math.max(Number(currentPage) || 1, 1);
24
+ const pages = Math.max(Number(totalPages) || 1, 1);
25
+ const count = Math.max(Number(totalCount) || 0, 0);
23
26
 
24
27
  const handlePerPageChange = (value) => {
25
28
  const url = new URL(window.location.href);
26
29
  url.searchParams.set("per_page", value);
27
30
  url.searchParams.delete("_page");
28
- remote(url.pathname + url.search, { pageKey });
31
+ visit(url.pathname + url.search, {});
29
32
  };
30
33
 
31
34
  return (
32
- <div className="flex items-center justify-between gap-4 py-4">
33
- <Field orientation="horizontal" className="w-fit">
34
- <FieldLabel htmlFor="select-rows-per-page">Rows per page</FieldLabel>
35
- <Select value={String(perPage)} onValueChange={handlePerPageChange}>
36
- <SelectTrigger className="w-20" id="select-rows-per-page">
37
- <SelectValue />
38
- </SelectTrigger>
39
- <SelectContent align="start">
40
- <SelectGroup>
41
- <SelectItem value="10">10</SelectItem>
42
- <SelectItem value="25">25</SelectItem>
43
- <SelectItem value="50">50</SelectItem>
44
- <SelectItem value="100">100</SelectItem>
45
- </SelectGroup>
46
- </SelectContent>
47
- </Select>
48
- </Field>
35
+ <div className="flex flex-col gap-3 py-4 sm:flex-row sm:items-center sm:justify-between">
36
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
37
+ <Field orientation="horizontal" className="w-fit">
38
+ <FieldLabel htmlFor="select-rows-per-page">Rows per page</FieldLabel>
39
+ <Select value={String(perPage)} onValueChange={handlePerPageChange}>
40
+ <SelectTrigger className="w-20" id="select-rows-per-page">
41
+ <SelectValue />
42
+ </SelectTrigger>
43
+ <SelectContent align="start">
44
+ <SelectGroup>
45
+ <SelectItem value="10">10</SelectItem>
46
+ <SelectItem value="25">25</SelectItem>
47
+ <SelectItem value="50">50</SelectItem>
48
+ <SelectItem value="100">100</SelectItem>
49
+ </SelectGroup>
50
+ </SelectContent>
51
+ </Select>
52
+ </Field>
53
+ <p className="text-sm text-muted-foreground" aria-live="polite">
54
+ Page {page} of {pages} - {count.toLocaleString()} total
55
+ </p>
56
+ </div>
49
57
  <PaginationRoot className="mx-0 w-auto">
50
58
  <PaginationContent>
51
59
  <PaginationItem>
52
60
  {prevPagePath ? (
53
- <PaginationPrevious href={prevPagePath} data-sg-remote />
61
+ <PaginationPrevious href={prevPagePath} data-sg-visit />
54
62
  ) : (
55
63
  <PaginationPrevious className="pointer-events-none opacity-50" />
56
64
  )}
57
65
  </PaginationItem>
58
66
  <PaginationItem>
59
67
  {nextPagePath ? (
60
- <PaginationNext href={nextPagePath} data-sg-remote />
68
+ <PaginationNext href={nextPagePath} data-sg-visit />
61
69
  ) : (
62
70
  <PaginationNext className="pointer-events-none opacity-50" />
63
71
  )}