terrazzo 0.2.1 → 0.2.3
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/views/terrazzo/application/index.json.props +4 -1
- data/app/views/terrazzo/application/show.json.props +19 -10
- data/lib/generators/terrazzo/views/templates/fields/has_many/ShowField.jsx +67 -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/field/has_many.rb +14 -12
- data/lib/terrazzo/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8912409821769ce6f2303842822a9d33696bef4a24c9fdc9974411a99dccc7b0
|
|
4
|
+
data.tar.gz: a171f982161da734439ac15c77cafa831ace5f8f9b8f87b43d93aa3107cf67d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1394dab4447f66fbda0801d3b60a74ddd8f0325b6927562c4f8cf7b33212491b029efc78b12d5d8eb887496fed9261b8abb3b75f4e5f2148d74a80061951643a
|
|
7
|
+
data.tar.gz: f16c4e076d8f693b789622073df1fd9ccb5bedf36afb4eaa1032c4da09e8d87cc69b5a4929c8dcea28d0228c2ef53fc7476e71c8e12eee1e98a8b057982bad4a
|
|
@@ -42,7 +42,10 @@ json.table do
|
|
|
42
42
|
json.attribute attr.to_s
|
|
43
43
|
json.fieldType field.field_type
|
|
44
44
|
json.value field.serialize_value(:index)
|
|
45
|
-
|
|
45
|
+
|
|
46
|
+
if field.class.associative? && field.data.present?
|
|
47
|
+
json.showPath polymorphic_path([namespace, field.data]) rescue nil
|
|
48
|
+
end
|
|
46
49
|
end
|
|
47
50
|
end
|
|
48
51
|
end
|
|
@@ -1,15 +1,28 @@
|
|
|
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
|
+
|
|
9
|
+
if field.class.associative? && field.data.present?
|
|
10
|
+
if field.is_a?(Terrazzo::Field::HasMany)
|
|
11
|
+
json.itemShowPaths(field.data.each_with_object({}) do |record, paths|
|
|
12
|
+
paths[record.id.to_s] = polymorphic_path([namespace, record]) rescue nil
|
|
13
|
+
end)
|
|
14
|
+
else
|
|
15
|
+
json.showPath polymorphic_path([namespace, field.data]) rescue nil
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
3
20
|
json.attributeGroups do
|
|
4
21
|
json.array! @page.grouped_attributes do |group|
|
|
5
22
|
json.name group[:name]
|
|
6
23
|
json.attributes do
|
|
7
24
|
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
|
|
25
|
+
show_field_json.call(json, field)
|
|
13
26
|
end
|
|
14
27
|
end
|
|
15
28
|
end
|
|
@@ -17,11 +30,7 @@ end
|
|
|
17
30
|
|
|
18
31
|
json.attributes do
|
|
19
32
|
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
|
|
33
|
+
show_field_json.call(json, field)
|
|
25
34
|
end
|
|
26
35
|
end
|
|
27
36
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useState, useContext } from "react";
|
|
2
|
+
import { NavigationContext } from "@thoughtbot/superglue";
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
Table,
|
|
@@ -8,17 +9,32 @@ import {
|
|
|
8
9
|
TableHead,
|
|
9
10
|
TableCell,
|
|
10
11
|
} from "../../components/ui/table";
|
|
12
|
+
import { Badge } from "../../components/ui/badge";
|
|
13
|
+
import { Button } from "../../components/ui/button";
|
|
11
14
|
import { FieldRenderer } from "../FieldRenderer";
|
|
12
15
|
|
|
13
|
-
export function ShowField({ value }) {
|
|
16
|
+
export function ShowField({ value, itemShowPaths }) {
|
|
14
17
|
if (!value) return <span className="text-muted-foreground">None</span>;
|
|
15
18
|
|
|
16
|
-
const { items, headers, total,
|
|
19
|
+
const { items, headers, total, initialLimit } = value;
|
|
20
|
+
const [expanded, setExpanded] = useState(false);
|
|
21
|
+
const { visit } = useContext(NavigationContext);
|
|
17
22
|
|
|
18
23
|
if (!items || items.length === 0) {
|
|
19
24
|
return <span className="text-muted-foreground">None</span>;
|
|
20
25
|
}
|
|
21
26
|
|
|
27
|
+
const pathFor = (id) => itemShowPaths?.[String(id)];
|
|
28
|
+
const hasMore = initialLimit && initialLimit > 0 && total > initialLimit;
|
|
29
|
+
const visibleItems = expanded || !hasMore ? items : items.slice(0, initialLimit);
|
|
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
|
+
|
|
22
38
|
// Table mode: collection_attributes specified
|
|
23
39
|
if (headers) {
|
|
24
40
|
return (
|
|
@@ -33,22 +49,38 @@ export function ShowField({ value }) {
|
|
|
33
49
|
</TableRow>
|
|
34
50
|
</TableHeader>
|
|
35
51
|
<TableBody>
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
{visibleItems.map((item) => {
|
|
53
|
+
const showPath = pathFor(item.id);
|
|
54
|
+
return (
|
|
55
|
+
<TableRow
|
|
56
|
+
key={item.id}
|
|
57
|
+
className={showPath ? "cursor-pointer" : ""}
|
|
58
|
+
onClick={(e) => handleRowClick(e, showPath)}>
|
|
59
|
+
{item.columns.map((col, colIndex) =>
|
|
60
|
+
<TableCell key={col.attribute}>
|
|
61
|
+
{showPath && colIndex === 0 ? (
|
|
62
|
+
<a href={showPath} data-sg-visit className="hover:underline">
|
|
63
|
+
<FieldRenderer mode="index" {...col} />
|
|
64
|
+
</a>
|
|
65
|
+
) : (
|
|
66
|
+
<FieldRenderer mode="index" {...col} />
|
|
67
|
+
)}
|
|
68
|
+
</TableCell>
|
|
69
|
+
)}
|
|
70
|
+
</TableRow>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
45
73
|
</TableBody>
|
|
46
74
|
</Table>
|
|
47
75
|
</div>
|
|
48
76
|
{hasMore && (
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
77
|
+
<Button
|
|
78
|
+
variant="link"
|
|
79
|
+
size="sm"
|
|
80
|
+
className="mt-2 px-0"
|
|
81
|
+
onClick={() => setExpanded(!expanded)}>
|
|
82
|
+
{expanded ? "Show less" : `Show ${total - initialLimit} more`}
|
|
83
|
+
</Button>
|
|
52
84
|
)}
|
|
53
85
|
</div>
|
|
54
86
|
);
|
|
@@ -57,16 +89,27 @@ export function ShowField({ value }) {
|
|
|
57
89
|
// Simple list mode
|
|
58
90
|
return (
|
|
59
91
|
<div>
|
|
60
|
-
<
|
|
61
|
-
{
|
|
62
|
-
|
|
92
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
93
|
+
{visibleItems.map((item) => {
|
|
94
|
+
const showPath = pathFor(item.id);
|
|
95
|
+
return showPath ? (
|
|
96
|
+
<a key={item.id} href={showPath} data-sg-visit>
|
|
97
|
+
<Badge variant="secondary" className="cursor-pointer">{item.display}</Badge>
|
|
98
|
+
</a>
|
|
99
|
+
) : (
|
|
100
|
+
<Badge key={item.id} variant="secondary">{item.display}</Badge>
|
|
101
|
+
);
|
|
102
|
+
})}
|
|
103
|
+
{hasMore && (
|
|
104
|
+
<Button
|
|
105
|
+
variant="link"
|
|
106
|
+
size="sm"
|
|
107
|
+
className="px-0"
|
|
108
|
+
onClick={() => setExpanded(!expanded)}>
|
|
109
|
+
{expanded ? "Show less" : `Show ${total - initialLimit} more`}
|
|
110
|
+
</Button>
|
|
63
111
|
)}
|
|
64
|
-
</
|
|
65
|
-
{hasMore && (
|
|
66
|
-
<p className="text-sm text-muted-foreground mt-2">
|
|
67
|
-
Showing {items.length} of {total}
|
|
68
|
-
</p>
|
|
69
|
-
)}
|
|
112
|
+
</div>
|
|
70
113
|
</div>
|
|
71
114
|
);
|
|
72
115
|
}
|
|
@@ -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
|
)}
|
|
@@ -42,26 +42,28 @@ module Terrazzo
|
|
|
42
42
|
|
|
43
43
|
def serialize_show_value
|
|
44
44
|
limit = options.fetch(:limit, 5)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
records = if limit && limit > 0
|
|
49
|
-
data.respond_to?(:limit) ? data.limit(limit) : data.first(limit)
|
|
50
|
-
else
|
|
51
|
-
data
|
|
52
|
-
end
|
|
45
|
+
all_records = data.to_a
|
|
46
|
+
total = all_records.size
|
|
47
|
+
col_attrs = options[:collection_attributes] || resolve_default_collection_attributes
|
|
53
48
|
|
|
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
|
|
64
59
|
|
|
60
|
+
def resolve_default_collection_attributes
|
|
61
|
+
dashboard_class = find_associated_dashboard
|
|
62
|
+
dashboard_class.new.collection_attributes
|
|
63
|
+
rescue NameError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
65
67
|
def serialize_with_collection_attributes(records, col_attrs, total, limit)
|
|
66
68
|
dashboard_class = find_associated_dashboard
|
|
67
69
|
|
|
@@ -85,7 +87,7 @@ module Terrazzo
|
|
|
85
87
|
headers: headers,
|
|
86
88
|
items: items,
|
|
87
89
|
total: total,
|
|
88
|
-
|
|
90
|
+
initialLimit: limit
|
|
89
91
|
}
|
|
90
92
|
end
|
|
91
93
|
|
data/lib/terrazzo/version.rb
CHANGED