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.
- checksums.yaml +4 -4
- data/README.md +101 -0
- data/Rakefile +1 -0
- data/app/controllers/terrazzo/application_controller.rb +63 -14
- data/app/helpers/terrazzo/collection_actions_helper.rb +38 -13
- data/app/helpers/terrazzo/resource_paths_helper.rb +55 -0
- data/app/views/terrazzo/application/_edit_base.json.props +7 -4
- data/app/views/terrazzo/application/_index_base.json.props +83 -7
- data/app/views/terrazzo/application/_navigation.json.props +7 -4
- data/app/views/terrazzo/application/_new_base.json.props +6 -3
- data/app/views/terrazzo/application/_show_base.json.props +7 -5
- data/lib/generators/terrazzo/dashboard/dashboard_generator.rb +48 -24
- data/lib/generators/terrazzo/dashboard/templates/controller.rb.erb +8 -4
- data/lib/generators/terrazzo/dashboard/templates/dashboard.rb.erb +33 -25
- data/lib/generators/terrazzo/eject/eject_generator.rb +223 -113
- data/lib/generators/terrazzo/field/field_generator.rb +51 -7
- data/lib/generators/terrazzo/field/templates/FormField.jsx.erb +2 -3
- data/lib/generators/terrazzo/install/install_generator.rb +641 -4
- data/lib/generators/terrazzo/install/templates/admin.css +2 -0
- data/lib/generators/terrazzo/install/templates/application.js.erb +3 -0
- data/lib/generators/terrazzo/install/templates/components.json.erb +17 -0
- data/lib/generators/terrazzo/install/templates/custom_page_mapping.js.erb +5 -0
- data/lib/generators/terrazzo/install/templates/generated_page_mapping.js.erb +10 -0
- data/lib/generators/terrazzo/install/templates/jsconfig.json +8 -0
- data/lib/generators/terrazzo/install/templates/page_to_page_mapping.js.erb +9 -1
- data/lib/generators/terrazzo/install/templates/superglue.html.erb.erb +2 -2
- data/lib/generators/terrazzo/routes/routes_generator.rb +55 -20
- data/lib/generators/terrazzo/views/edit_generator.rb +16 -10
- data/lib/generators/terrazzo/views/eject_compatibility.rb +19 -0
- data/lib/generators/terrazzo/views/field_generator.rb +18 -6
- data/lib/generators/terrazzo/views/generated_defaults_helper.rb +110 -0
- data/lib/generators/terrazzo/views/index_generator.rb +9 -8
- data/lib/generators/terrazzo/views/layout_generator.rb +4 -3
- data/lib/generators/terrazzo/views/navigation_generator.rb +5 -1
- data/lib/generators/terrazzo/views/new_generator.rb +16 -10
- data/lib/generators/terrazzo/views/page_mapping_helper.rb +131 -8
- data/lib/generators/terrazzo/views/show_generator.rb +7 -6
- data/lib/generators/terrazzo/views/templates/components/CollectionFilters.jsx +25 -0
- data/lib/generators/terrazzo/views/templates/components/CollectionItemActions.jsx +68 -0
- data/lib/generators/terrazzo/views/templates/components/CollectionToolbarActions.jsx +60 -0
- data/lib/generators/terrazzo/views/templates/components/HasManyPagination.jsx +36 -0
- data/lib/generators/terrazzo/views/templates/components/Layout.jsx +9 -4
- data/lib/generators/terrazzo/views/templates/components/Pagination.jsx +32 -24
- data/lib/generators/terrazzo/views/templates/components/ResourceTable.jsx +172 -0
- data/lib/generators/terrazzo/views/templates/components/SearchBar.jsx +14 -6
- data/lib/generators/terrazzo/views/templates/components/SortableHeader.jsx +7 -5
- data/lib/generators/terrazzo/views/templates/components/app-sidebar.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/components/site-header.jsx +2 -2
- data/lib/generators/terrazzo/views/templates/fields/asset/FormField.jsx +15 -0
- data/lib/generators/terrazzo/views/templates/fields/asset/IndexField.jsx +5 -0
- data/lib/generators/terrazzo/views/templates/fields/asset/ShowField.jsx +13 -0
- data/lib/generators/terrazzo/views/templates/fields/belongs_to/FormField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/boolean/FormField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/boolean/IndexField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/has_many/FormField.jsx +3 -3
- data/lib/generators/terrazzo/views/templates/fields/has_many/IndexField.jsx +4 -3
- data/lib/generators/terrazzo/views/templates/fields/has_many/ShowField.jsx +21 -35
- data/lib/generators/terrazzo/views/templates/fields/has_one/FormField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/hstore/FormField.jsx +5 -5
- data/lib/generators/terrazzo/views/templates/fields/hstore/IndexField.jsx +0 -2
- data/lib/generators/terrazzo/views/templates/fields/hstore/ShowField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/number/IndexField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/number/ShowField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/polymorphic/FormField.jsx +3 -2
- data/lib/generators/terrazzo/views/templates/fields/rich_text/FormField.jsx +2 -2
- data/lib/generators/terrazzo/views/templates/fields/select/FormField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/select/IndexField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/shared/TextInputFormField.jsx +2 -2
- data/lib/generators/terrazzo/views/templates/fields/text/FormField.jsx +2 -2
- data/lib/generators/terrazzo/views/templates/pages/_collection.jsx +5 -3
- data/lib/generators/terrazzo/views/templates/pages/_form.jsx +37 -30
- data/lib/generators/terrazzo/views/templates/pages/_navigation.json.props +14 -4
- data/lib/generators/terrazzo/views/templates/pages/edit.jsx +6 -4
- data/lib/generators/terrazzo/views/templates/pages/index.jsx +20 -8
- data/lib/generators/terrazzo/views/templates/pages/new.jsx +8 -4
- data/lib/generators/terrazzo/views/templates/pages/show.jsx +30 -32
- data/lib/generators/terrazzo/views/views_generator.rb +9 -5
- data/lib/terrazzo/base_dashboard.rb +167 -5
- data/lib/terrazzo/csv_export.rb +38 -0
- data/lib/terrazzo/field/associative.rb +16 -4
- data/lib/terrazzo/field/base.rb +4 -0
- data/lib/terrazzo/field/belongs_to.rb +1 -2
- data/lib/terrazzo/field/deferred.rb +4 -0
- data/lib/terrazzo/field/has_many.rb +79 -11
- data/lib/terrazzo/field/password.rb +6 -0
- data/lib/terrazzo/field/rich_text.rb +13 -0
- data/lib/terrazzo/generator_helpers.rb +1 -1
- data/lib/terrazzo/has_many_pagination.rb +9 -1
- data/lib/terrazzo/namespace/resource.rb +30 -0
- data/lib/terrazzo/namespace.rb +26 -1
- data/lib/terrazzo/search.rb +29 -9
- data/lib/terrazzo/version.rb +1 -1
- data/lib/terrazzo.rb +1 -0
- data/terrazzo.gemspec +11 -1
- metadata +40 -6
- data/lib/generators/terrazzo/install/templates/application.json.props.erb +0 -17
- data/lib/generators/terrazzo/views/templates/fields/FieldRenderer.jsx +0 -103
- 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 =
|
|
9
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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} · {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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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 "
|
|
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 "
|
|
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 "
|
|
19
|
+
} from "./ui";
|
|
20
20
|
|
|
21
21
|
export function Pagination({ currentPage, totalPages, totalCount, perPage, nextPagePath, prevPagePath }) {
|
|
22
|
-
const {
|
|
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
|
-
|
|
31
|
+
visit(url.pathname + url.search, {});
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
return (
|
|
32
|
-
<div className="flex
|
|
33
|
-
<
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
<
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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-
|
|
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-
|
|
68
|
+
<PaginationNext href={nextPagePath} data-sg-visit />
|
|
61
69
|
) : (
|
|
62
70
|
<PaginationNext className="pointer-events-none opacity-50" />
|
|
63
71
|
)}
|