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
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
import { FieldRenderer } from "
|
|
4
|
-
import { Button } from "
|
|
3
|
+
import { FieldRenderer } from "../fields";
|
|
4
|
+
import { Button } from "../components/ui";
|
|
5
5
|
|
|
6
|
-
export function AdminForm({ form, errors }) {
|
|
7
|
-
const { props: formProps, extras, fieldGroups, fields } = form;
|
|
6
|
+
export function AdminForm({ form = {}, errors = [] }) {
|
|
7
|
+
const { props: formProps = {}, extras = {}, fieldGroups = [], fields = [] } = form;
|
|
8
|
+
const groups = Array.isArray(fieldGroups) ? fieldGroups : [];
|
|
9
|
+
const hasFieldGroups = groups.length > 0;
|
|
10
|
+
|
|
11
|
+
const renderField = (field) => {
|
|
12
|
+
const hintId = field.hint && field.input?.id ? `${field.input.id}_hint` : null;
|
|
13
|
+
const input = hintId ?
|
|
14
|
+
{ ...field.input, "aria-describedby": hintId } :
|
|
15
|
+
field.input;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div key={field.attribute} className="space-y-2">
|
|
19
|
+
<FieldRenderer
|
|
20
|
+
mode="form"
|
|
21
|
+
fieldType={field.fieldType}
|
|
22
|
+
value={field.value}
|
|
23
|
+
options={field.options}
|
|
24
|
+
attribute={field.attribute}
|
|
25
|
+
label={field.label}
|
|
26
|
+
hint={field.hint}
|
|
27
|
+
input={input}
|
|
28
|
+
required={field.required} />
|
|
29
|
+
{field.hint && (
|
|
30
|
+
<p id={hintId ?? undefined} className="text-sm text-muted-foreground">
|
|
31
|
+
{field.hint}
|
|
32
|
+
</p>
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
8
37
|
|
|
9
38
|
return (
|
|
10
39
|
<>
|
|
@@ -26,44 +55,22 @@ export function AdminForm({ form, errors }) {
|
|
|
26
55
|
<input key={hiddenProps.name} {...hiddenProps} />
|
|
27
56
|
)}
|
|
28
57
|
|
|
29
|
-
{
|
|
58
|
+
{hasFieldGroups ? (
|
|
30
59
|
<div className="space-y-8">
|
|
31
|
-
{
|
|
60
|
+
{groups.map((group, groupIndex) =>
|
|
32
61
|
<fieldset key={groupIndex} className="space-y-6">
|
|
33
62
|
{group.name && (
|
|
34
63
|
<legend className="text-lg font-semibold">{group.name}</legend>
|
|
35
64
|
)}
|
|
36
65
|
<div className="space-y-6">
|
|
37
|
-
{group.fields.map((field) =>
|
|
38
|
-
<FieldRenderer
|
|
39
|
-
key={field.attribute}
|
|
40
|
-
mode="form"
|
|
41
|
-
fieldType={field.fieldType}
|
|
42
|
-
value={field.value}
|
|
43
|
-
options={field.options}
|
|
44
|
-
attribute={field.attribute}
|
|
45
|
-
label={field.label}
|
|
46
|
-
input={field.input}
|
|
47
|
-
required={field.required} />
|
|
48
|
-
)}
|
|
66
|
+
{(group.fields || []).map((field) => renderField(field))}
|
|
49
67
|
</div>
|
|
50
68
|
</fieldset>
|
|
51
69
|
)}
|
|
52
70
|
</div>
|
|
53
71
|
) : (
|
|
54
72
|
<div className="space-y-6">
|
|
55
|
-
{fields.map((field) =>
|
|
56
|
-
<FieldRenderer
|
|
57
|
-
key={field.attribute}
|
|
58
|
-
mode="form"
|
|
59
|
-
fieldType={field.fieldType}
|
|
60
|
-
value={field.value}
|
|
61
|
-
options={field.options}
|
|
62
|
-
attribute={field.attribute}
|
|
63
|
-
label={field.label}
|
|
64
|
-
input={field.input}
|
|
65
|
-
required={field.required} />
|
|
66
|
-
)}
|
|
73
|
+
{fields.map((field) => renderField(field))}
|
|
67
74
|
</div>
|
|
68
75
|
)}
|
|
69
76
|
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
resources = Terrazzo::Namespace.new(namespace).navigation_resources
|
|
2
|
+
navigation_groups = resources.group_by(&:navigation_group).map do |label, items|
|
|
3
|
+
{ label: label, items: items }
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
json.array! navigation_groups do |group|
|
|
7
|
+
json.label group[:label]
|
|
8
|
+
json.items do
|
|
9
|
+
json.array! group[:items] do |nav_resource|
|
|
10
|
+
json.label nav_resource.navigation_label
|
|
11
|
+
json.path url_for(controller: "/#{nav_resource.controller_path}", action: :index, only_path: true)
|
|
12
|
+
json.active nav_resource.controller_path == controller_path
|
|
13
|
+
end
|
|
14
|
+
end
|
|
5
15
|
end
|
|
@@ -3,7 +3,8 @@ import { useContent } from "@thoughtbot/superglue";
|
|
|
3
3
|
|
|
4
4
|
import { getLayout } from "terrazzo";
|
|
5
5
|
import { AdminForm } from "./_form";
|
|
6
|
-
import {
|
|
6
|
+
import { CollectionToolbarActions } from "../components";
|
|
7
|
+
import { Button, Card, CardContent } from "../components/ui";
|
|
7
8
|
|
|
8
9
|
export default function AdminEdit() {
|
|
9
10
|
const Layout = getLayout();
|
|
@@ -11,10 +12,10 @@ export default function AdminEdit() {
|
|
|
11
12
|
pageTitle,
|
|
12
13
|
form,
|
|
13
14
|
errors,
|
|
15
|
+
layoutActions,
|
|
14
16
|
indexPath,
|
|
15
17
|
showPath,
|
|
16
|
-
navigation
|
|
17
|
-
resourceName
|
|
18
|
+
navigation
|
|
18
19
|
} = useContent();
|
|
19
20
|
|
|
20
21
|
return (
|
|
@@ -22,7 +23,8 @@ export default function AdminEdit() {
|
|
|
22
23
|
navigation={navigation}
|
|
23
24
|
title={pageTitle}
|
|
24
25
|
actions={
|
|
25
|
-
<div className="flex gap-2">
|
|
26
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
27
|
+
<CollectionToolbarActions actions={layoutActions} />
|
|
26
28
|
{showPath &&
|
|
27
29
|
<a href={showPath} data-sg-visit>
|
|
28
30
|
<Button variant="outline" size="sm">Cancel</Button>
|
|
@@ -2,16 +2,21 @@ import React from "react";
|
|
|
2
2
|
import { useContent } from "@thoughtbot/superglue";
|
|
3
3
|
|
|
4
4
|
import { getLayout } from "terrazzo";
|
|
5
|
-
import { SearchBar, Pagination } from "
|
|
5
|
+
import { SearchBar, CollectionFilters, Pagination, CollectionToolbarActions } from "../components";
|
|
6
6
|
import { AdminCollection } from "./_collection";
|
|
7
|
-
import { Button } from "
|
|
7
|
+
import { Button } from "../components/ui";
|
|
8
8
|
|
|
9
9
|
export default function AdminIndex() {
|
|
10
10
|
const Layout = getLayout();
|
|
11
11
|
const {
|
|
12
12
|
table,
|
|
13
13
|
searchBar,
|
|
14
|
+
filters,
|
|
14
15
|
pagination,
|
|
16
|
+
layoutActions,
|
|
17
|
+
toolbarActions,
|
|
18
|
+
bulkActions,
|
|
19
|
+
emptyState,
|
|
15
20
|
navigation,
|
|
16
21
|
newResourcePath,
|
|
17
22
|
resourceName,
|
|
@@ -22,17 +27,24 @@ export default function AdminIndex() {
|
|
|
22
27
|
<Layout
|
|
23
28
|
navigation={navigation}
|
|
24
29
|
title={resourceName}
|
|
25
|
-
actions={
|
|
26
|
-
|
|
27
|
-
<
|
|
28
|
-
|
|
30
|
+
actions={
|
|
31
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
32
|
+
<CollectionToolbarActions actions={layoutActions} />
|
|
33
|
+
<CollectionToolbarActions actions={toolbarActions} />
|
|
34
|
+
{newResourcePath && (
|
|
35
|
+
<a href={newResourcePath} data-sg-visit>
|
|
36
|
+
<Button size="sm">New {singularResourceName}</Button>
|
|
37
|
+
</a>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
29
40
|
}>
|
|
30
41
|
|
|
31
|
-
<div className="flex items-center">
|
|
42
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
32
43
|
<SearchBar {...searchBar} />
|
|
44
|
+
<CollectionFilters {...filters} />
|
|
33
45
|
</div>
|
|
34
46
|
|
|
35
|
-
<AdminCollection table={table} />
|
|
47
|
+
<AdminCollection table={table} emptyState={emptyState} bulkActions={bulkActions} />
|
|
36
48
|
|
|
37
49
|
<Pagination {...pagination} />
|
|
38
50
|
</Layout>);
|
|
@@ -3,7 +3,8 @@ import { useContent } from "@thoughtbot/superglue";
|
|
|
3
3
|
|
|
4
4
|
import { getLayout } from "terrazzo";
|
|
5
5
|
import { AdminForm } from "./_form";
|
|
6
|
-
import {
|
|
6
|
+
import { CollectionToolbarActions } from "../components";
|
|
7
|
+
import { Button, Card, CardContent } from "../components/ui";
|
|
7
8
|
|
|
8
9
|
export default function AdminNew() {
|
|
9
10
|
const Layout = getLayout();
|
|
@@ -11,9 +12,9 @@ export default function AdminNew() {
|
|
|
11
12
|
pageTitle,
|
|
12
13
|
form,
|
|
13
14
|
errors,
|
|
15
|
+
layoutActions,
|
|
14
16
|
indexPath,
|
|
15
|
-
navigation
|
|
16
|
-
resourceName
|
|
17
|
+
navigation
|
|
17
18
|
} = useContent();
|
|
18
19
|
|
|
19
20
|
return (
|
|
@@ -21,9 +22,12 @@ export default function AdminNew() {
|
|
|
21
22
|
navigation={navigation}
|
|
22
23
|
title={pageTitle}
|
|
23
24
|
actions={
|
|
24
|
-
<
|
|
25
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
26
|
+
<CollectionToolbarActions actions={layoutActions} />
|
|
27
|
+
<a href={indexPath} data-sg-visit>
|
|
25
28
|
<Button variant="outline" size="sm">Cancel</Button>
|
|
26
29
|
</a>
|
|
30
|
+
</div>
|
|
27
31
|
}>
|
|
28
32
|
|
|
29
33
|
<Card>
|
|
@@ -2,8 +2,9 @@ import React from "react";
|
|
|
2
2
|
import { useContent } from "@thoughtbot/superglue";
|
|
3
3
|
|
|
4
4
|
import { getLayout } from "terrazzo";
|
|
5
|
-
import { FieldRenderer } from "
|
|
6
|
-
import {
|
|
5
|
+
import { FieldRenderer } from "../fields";
|
|
6
|
+
import { CollectionToolbarActions } from "../components";
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui";
|
|
7
8
|
|
|
8
9
|
export default function AdminShow() {
|
|
9
10
|
const Layout = getLayout();
|
|
@@ -11,47 +12,41 @@ export default function AdminShow() {
|
|
|
11
12
|
pageTitle,
|
|
12
13
|
attributes,
|
|
13
14
|
attributeGroups,
|
|
15
|
+
layoutActions,
|
|
14
16
|
editPath,
|
|
15
17
|
deletePath,
|
|
16
18
|
indexPath,
|
|
17
|
-
resourceName,
|
|
18
19
|
pluralResourceName,
|
|
19
20
|
navigation
|
|
20
21
|
} = useContent();
|
|
22
|
+
const resourceActions = [
|
|
23
|
+
indexPath && {
|
|
24
|
+
label: `Back to ${pluralResourceName}`,
|
|
25
|
+
url: indexPath,
|
|
26
|
+
variant: "outline",
|
|
27
|
+
},
|
|
28
|
+
editPath && {
|
|
29
|
+
label: "Edit",
|
|
30
|
+
url: editPath,
|
|
31
|
+
variant: "outline",
|
|
32
|
+
},
|
|
33
|
+
deletePath && {
|
|
34
|
+
label: "Delete",
|
|
35
|
+
url: deletePath,
|
|
36
|
+
method: "delete",
|
|
37
|
+
confirm: "Are you sure?",
|
|
38
|
+
variant: "destructive",
|
|
39
|
+
},
|
|
40
|
+
].filter(Boolean);
|
|
21
41
|
|
|
22
42
|
return (
|
|
23
43
|
<Layout
|
|
24
44
|
navigation={navigation}
|
|
25
45
|
title={pageTitle}
|
|
26
46
|
actions={
|
|
27
|
-
<div className="flex gap-2">
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
</a>
|
|
31
|
-
{editPath &&
|
|
32
|
-
<a href={editPath} data-sg-visit>
|
|
33
|
-
<Button variant="outline" size="sm">Edit</Button>
|
|
34
|
-
</a>
|
|
35
|
-
}
|
|
36
|
-
{deletePath &&
|
|
37
|
-
<form
|
|
38
|
-
action={deletePath}
|
|
39
|
-
method="post"
|
|
40
|
-
data-sg-visit
|
|
41
|
-
style={{ display: "inline" }}
|
|
42
|
-
onSubmit={(e) => {
|
|
43
|
-
if (!window.confirm("Are you sure?")) e.preventDefault();
|
|
44
|
-
}}>
|
|
45
|
-
|
|
46
|
-
<input type="hidden" name="_method" value="delete" />
|
|
47
|
-
<input
|
|
48
|
-
type="hidden"
|
|
49
|
-
name="authenticity_token"
|
|
50
|
-
value={document.querySelector('meta[name="csrf-token"]')?.content ?? ""} />
|
|
51
|
-
|
|
52
|
-
<Button type="submit" variant="destructive" size="sm">Delete</Button>
|
|
53
|
-
</form>
|
|
54
|
-
}
|
|
47
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
48
|
+
<CollectionToolbarActions actions={layoutActions} />
|
|
49
|
+
<CollectionToolbarActions actions={resourceActions} />
|
|
55
50
|
</div>
|
|
56
51
|
}>
|
|
57
52
|
|
|
@@ -69,7 +64,10 @@ export default function AdminShow() {
|
|
|
69
64
|
return (
|
|
70
65
|
<div key={key} className="py-4 grid grid-cols-3 gap-4">
|
|
71
66
|
<dt className="text-sm font-medium text-muted-foreground">
|
|
72
|
-
{attr.label}
|
|
67
|
+
<span>{attr.label}</span>
|
|
68
|
+
{attr.hint && (
|
|
69
|
+
<p className="mt-1 font-normal">{attr.hint}</p>
|
|
70
|
+
)}
|
|
73
71
|
</dt>
|
|
74
72
|
<dd className="col-span-2 text-sm">
|
|
75
73
|
{attr.showPath ? (
|
|
@@ -27,20 +27,24 @@ module Terrazzo
|
|
|
27
27
|
def create_ui_barrel
|
|
28
28
|
create_file "app/views/#{namespace_name}/components/ui/index.js", <<~JS
|
|
29
29
|
// Re-export all UI primitives from the terrazzo package.
|
|
30
|
-
//
|
|
30
|
+
// Ejected pages, fields, and components import this barrel.
|
|
31
|
+
// To customize those app-owned files, run: rails g terrazzo:eject ui/<component_name>
|
|
31
32
|
export * from "terrazzo/ui";
|
|
32
33
|
JS
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def create_navigation_partial
|
|
36
37
|
create_file "app/views/#{namespace_name}/application/_navigation.json.props", <<~RUBY
|
|
37
|
-
resources = Terrazzo::Namespace.new(namespace).
|
|
38
|
+
resources = Terrazzo::Namespace.new(namespace).navigation_resources
|
|
39
|
+
navigation_groups = resources.group_by(&:navigation_group).map do |label, items|
|
|
40
|
+
{ label: label, items: items }
|
|
41
|
+
end
|
|
38
42
|
|
|
39
|
-
json.array!
|
|
43
|
+
json.array! navigation_groups do |group|
|
|
40
44
|
json.label group[:label]
|
|
41
45
|
json.items do
|
|
42
|
-
json.array! group[:
|
|
43
|
-
json.label r.
|
|
46
|
+
json.array! group[:items] do |r|
|
|
47
|
+
json.label r.navigation_label
|
|
44
48
|
json.path url_for(controller: "/\#{r.controller_path}", action: :index, only_path: true)
|
|
45
49
|
json.active r.controller_path == controller_path
|
|
46
50
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "set"
|
|
2
|
+
|
|
1
3
|
module Terrazzo
|
|
2
4
|
class BaseDashboard
|
|
3
5
|
Field = Terrazzo::Field
|
|
@@ -66,16 +68,41 @@ module Terrazzo
|
|
|
66
68
|
"#{resource.class.name} ##{resource.id}"
|
|
67
69
|
end
|
|
68
70
|
|
|
71
|
+
def attribute_label(attribute, _context = nil)
|
|
72
|
+
attribute.to_s.humanize
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def attribute_hint(_attribute, _context = nil)
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def collection_cell_options(_attribute, _resource)
|
|
80
|
+
{}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def collection_header_options(_attribute)
|
|
84
|
+
{}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def collection_row_options(_resource)
|
|
88
|
+
{}
|
|
89
|
+
end
|
|
90
|
+
|
|
69
91
|
def collection_includes
|
|
70
|
-
|
|
92
|
+
includes_for_attributes(collection_attributes)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def includes_for_attributes(attributes)
|
|
96
|
+
attribute_set = Set.new(flatten_attributes(attributes))
|
|
71
97
|
model = self.class.model
|
|
72
98
|
attribute_types.each_with_object([]) do |(attr, type), includes|
|
|
73
|
-
next unless
|
|
99
|
+
next unless attribute_set.include?(attr)
|
|
74
100
|
next unless type.eager_load?
|
|
75
|
-
|
|
76
|
-
|
|
101
|
+
include_target = type.eager_load_association(attr, model)
|
|
102
|
+
has_association = model.reflect_on_association(include_target)
|
|
103
|
+
has_attachment = model.respond_to?(:reflect_on_attachment) && model.reflect_on_attachment(include_target)
|
|
77
104
|
next unless has_association || has_attachment
|
|
78
|
-
includes <<
|
|
105
|
+
includes << include_target
|
|
79
106
|
end
|
|
80
107
|
end
|
|
81
108
|
|
|
@@ -84,6 +111,141 @@ module Terrazzo
|
|
|
84
111
|
self.class::COLLECTION_FILTERS
|
|
85
112
|
end
|
|
86
113
|
|
|
114
|
+
def collection_filter_options(_view = nil)
|
|
115
|
+
collection_filters.filter_map do |name, filter|
|
|
116
|
+
next if filter.respond_to?(:arity) && filter.arity > 1
|
|
117
|
+
|
|
118
|
+
{
|
|
119
|
+
label: collection_filter_label(name),
|
|
120
|
+
value: name.to_s,
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def collection_filter_label(filter_name)
|
|
126
|
+
filter_name.to_s.humanize
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def collection_item_actions(resource, view = nil)
|
|
130
|
+
return [] unless view
|
|
131
|
+
|
|
132
|
+
actions = []
|
|
133
|
+
if (path = view.collection_action_path(resource, :show))
|
|
134
|
+
actions << { label: "Show", url: path }
|
|
135
|
+
end
|
|
136
|
+
if (path = view.collection_action_path(resource, :edit))
|
|
137
|
+
actions << { label: "Edit", url: path }
|
|
138
|
+
end
|
|
139
|
+
if (path = view.collection_action_path(resource, :destroy))
|
|
140
|
+
actions << {
|
|
141
|
+
label: "Destroy",
|
|
142
|
+
url: path,
|
|
143
|
+
method: "delete",
|
|
144
|
+
confirm: "Are you sure?"
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
actions
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def collection_toolbar_actions(view = nil)
|
|
151
|
+
return [] unless view && csv_export_enabled?
|
|
152
|
+
|
|
153
|
+
[
|
|
154
|
+
{
|
|
155
|
+
label: "Export CSV",
|
|
156
|
+
url: csv_export_url(view),
|
|
157
|
+
sg_visit: false,
|
|
158
|
+
},
|
|
159
|
+
]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def collection_bulk_actions(_view = nil)
|
|
163
|
+
[]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def layout_actions(_page, _view = nil, resource: nil)
|
|
167
|
+
[]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def csv_export_enabled?
|
|
171
|
+
true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def csv_attributes
|
|
175
|
+
collection_attributes
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def csv_filename
|
|
179
|
+
"#{self.class.resource_name.pluralize.parameterize}.csv"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def csv_value(_attribute, value, _resource)
|
|
183
|
+
case value
|
|
184
|
+
when nil
|
|
185
|
+
nil
|
|
186
|
+
when Hash
|
|
187
|
+
csv_hash_value(value)
|
|
188
|
+
when Array
|
|
189
|
+
value.map { |item| csv_value(nil, item, nil) }.join(", ")
|
|
190
|
+
else
|
|
191
|
+
value.to_s
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def empty_collection_message
|
|
196
|
+
"No #{self.class.resource_name.pluralize.downcase} match the current view."
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def navigation_label
|
|
200
|
+
self.class.const_defined?(:NAVIGATION_LABEL, false) ?
|
|
201
|
+
self.class::NAVIGATION_LABEL :
|
|
202
|
+
self.class.resource_name.pluralize
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def navigation_group
|
|
206
|
+
self.class.const_defined?(:NAVIGATION_GROUP, false) ?
|
|
207
|
+
self.class::NAVIGATION_GROUP :
|
|
208
|
+
default_navigation_group
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def navigation_order
|
|
212
|
+
self.class.const_defined?(:NAVIGATION_ORDER, false) ?
|
|
213
|
+
self.class::NAVIGATION_ORDER :
|
|
214
|
+
navigation_label
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def navigation_group_order
|
|
218
|
+
self.class.const_defined?(:NAVIGATION_GROUP_ORDER, false) ?
|
|
219
|
+
self.class::NAVIGATION_GROUP_ORDER :
|
|
220
|
+
navigation_group
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def show_in_navigation?
|
|
224
|
+
self.class.const_defined?(:SHOW_IN_NAVIGATION, false) ?
|
|
225
|
+
self.class::SHOW_IN_NAVIGATION :
|
|
226
|
+
true
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
def default_navigation_group
|
|
232
|
+
model_namespace = self.class.model.name.deconstantize
|
|
233
|
+
model_namespace.present? ? model_namespace.underscore.humanize.titleize : "Resources"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def csv_export_url(view)
|
|
237
|
+
query = view.request.query_parameters.except("_page", "per_page")
|
|
238
|
+
view.url_for(query.merge(format: :csv, only_path: true))
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def csv_hash_value(value)
|
|
242
|
+
indifferent = value.with_indifferent_access
|
|
243
|
+
return indifferent[:display].to_s if indifferent.key?(:display)
|
|
244
|
+
return "#{indifferent[:count]} #{indifferent[:label]}" if indifferent.key?(:count) && indifferent.key?(:label)
|
|
245
|
+
|
|
246
|
+
JSON.generate(value)
|
|
247
|
+
end
|
|
248
|
+
|
|
87
249
|
class << self
|
|
88
250
|
def model
|
|
89
251
|
name.chomp("Dashboard").constantize
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require "csv"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Terrazzo
|
|
5
|
+
class CsvExport
|
|
6
|
+
attr_reader :dashboard, :resources
|
|
7
|
+
|
|
8
|
+
def initialize(dashboard, resources)
|
|
9
|
+
@dashboard = dashboard
|
|
10
|
+
@resources = resources
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_csv
|
|
14
|
+
CSV.generate(headers: true) do |csv|
|
|
15
|
+
csv << attributes.map { |attr| header_for(attr) }
|
|
16
|
+
|
|
17
|
+
resources.each do |resource|
|
|
18
|
+
csv << attributes.map { |attr| value_for(resource, attr) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def attributes
|
|
26
|
+
@attributes ||= dashboard.flatten_attributes(dashboard.csv_attributes)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def header_for(attribute)
|
|
30
|
+
dashboard.attribute_label(attribute, :csv)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def value_for(resource, attribute)
|
|
34
|
+
field = dashboard.attribute_type_for(attribute).new(attribute, nil, :index, resource: resource)
|
|
35
|
+
dashboard.csv_value(attribute, field.serialize_value(:index), resource)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -14,6 +14,13 @@ module Terrazzo
|
|
|
14
14
|
private
|
|
15
15
|
|
|
16
16
|
def display_name(record)
|
|
17
|
+
dashboard = dashboard_for(record.class)
|
|
18
|
+
return dashboard.display_resource(record) if dashboard && custom_display_resource?(dashboard)
|
|
19
|
+
|
|
20
|
+
fallback_display_name(record)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def fallback_display_name(record)
|
|
17
24
|
if record.respond_to?(:display_name)
|
|
18
25
|
record.display_name
|
|
19
26
|
elsif record.respond_to?(:name)
|
|
@@ -44,16 +51,21 @@ module Terrazzo
|
|
|
44
51
|
associated_class.all
|
|
45
52
|
end
|
|
46
53
|
pk = association_primary_key
|
|
47
|
-
|
|
48
|
-
scope.map { |r| [dashboard ? dashboard.display_resource(r) : display_name(r), r.public_send(pk).to_s] }
|
|
54
|
+
scope.map { |r| [display_name(r), r.public_send(pk).to_s] }
|
|
49
55
|
end
|
|
50
56
|
|
|
51
|
-
def
|
|
52
|
-
|
|
57
|
+
def dashboard_for(klass)
|
|
58
|
+
return nil unless klass
|
|
59
|
+
|
|
60
|
+
"#{klass.name}Dashboard".constantize.new
|
|
53
61
|
rescue NameError
|
|
54
62
|
nil
|
|
55
63
|
end
|
|
56
64
|
|
|
65
|
+
def custom_display_resource?(dashboard)
|
|
66
|
+
dashboard.method(:display_resource).owner != Terrazzo::BaseDashboard
|
|
67
|
+
end
|
|
68
|
+
|
|
57
69
|
def association_primary_key
|
|
58
70
|
reflection = resource&.class&.reflect_on_association(attribute)
|
|
59
71
|
reflection&.association_primary_key || :id
|
data/lib/terrazzo/field/base.rb
CHANGED
|
@@ -46,8 +46,7 @@ module Terrazzo
|
|
|
46
46
|
end
|
|
47
47
|
scope = scope.reorder(options[:order]) if options[:order]
|
|
48
48
|
pk = association_primary_key
|
|
49
|
-
|
|
50
|
-
scope.map { |r| [dashboard ? dashboard.display_resource(r) : display_name(r), r.public_send(pk).to_s] }
|
|
49
|
+
scope.map { |r| [display_name(r), r.public_send(pk).to_s] }
|
|
51
50
|
end
|
|
52
51
|
|
|
53
52
|
def foreign_key_value
|