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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import React, { useContext, useState } from "react";
|
|
2
|
+
import { NavigationContext } from "@thoughtbot/superglue";
|
|
3
|
+
|
|
4
|
+
import { SortableHeader as DefaultSortableHeader } from "./SortableHeader";
|
|
5
|
+
import { CollectionItemActions as DefaultCollectionItemActions } from "./CollectionItemActions";
|
|
6
|
+
import { CollectionToolbarActions as DefaultCollectionToolbarActions } from "./CollectionToolbarActions";
|
|
7
|
+
import { getComponent, cn } from "terrazzo";
|
|
8
|
+
import { FieldRenderer } from "../fields";
|
|
9
|
+
import {
|
|
10
|
+
Table,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableBody,
|
|
13
|
+
TableRow,
|
|
14
|
+
TableHead,
|
|
15
|
+
TableCell,
|
|
16
|
+
} from "./ui";
|
|
17
|
+
|
|
18
|
+
export function ResourceTable({ headers = [], rows = [], emptyState, showActions = true, bulkActions = [] }) {
|
|
19
|
+
const { visit } = useContext(NavigationContext);
|
|
20
|
+
const SortableHeader = getComponent("SortableHeader") || DefaultSortableHeader;
|
|
21
|
+
const CollectionItemActions = getComponent("CollectionItemActions") || DefaultCollectionItemActions;
|
|
22
|
+
const CollectionToolbarActions = getComponent("CollectionToolbarActions") || DefaultCollectionToolbarActions;
|
|
23
|
+
const safeHeaders = Array.isArray(headers) ? headers : [];
|
|
24
|
+
const safeRows = Array.isArray(rows) ? rows : [];
|
|
25
|
+
const safeBulkActions = Array.isArray(bulkActions) ? bulkActions : [];
|
|
26
|
+
const selectable = safeBulkActions.length > 0 && safeRows.length > 0;
|
|
27
|
+
const rowIds = safeRows.map((row) => String(row.id));
|
|
28
|
+
const [selectedIds, setSelectedIds] = useState(() => new Set());
|
|
29
|
+
const selectedRowIds = rowIds.filter((id) => selectedIds.has(id));
|
|
30
|
+
const selectedCount = selectedRowIds.length;
|
|
31
|
+
const allSelected = selectable && selectedCount === rowIds.length;
|
|
32
|
+
const someSelected = selectedCount > 0 && !allSelected;
|
|
33
|
+
const columnCount = safeHeaders.length + (showActions ? 1 : 0) + (selectable ? 1 : 0);
|
|
34
|
+
|
|
35
|
+
const handleRowClick = (e, showPath) => {
|
|
36
|
+
if (!showPath) return;
|
|
37
|
+
if (e.target.closest("a, button, form, input, label")) return;
|
|
38
|
+
if (window.getSelection().toString()) return;
|
|
39
|
+
visit(showPath, {});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const toggleRow = (id, checked) => {
|
|
43
|
+
setSelectedIds((current) => {
|
|
44
|
+
const next = new Set(current);
|
|
45
|
+
if (checked) {
|
|
46
|
+
next.add(id);
|
|
47
|
+
} else {
|
|
48
|
+
next.delete(id);
|
|
49
|
+
}
|
|
50
|
+
return next;
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const toggleAllRows = (checked) => {
|
|
55
|
+
setSelectedIds((current) => {
|
|
56
|
+
const next = new Set(current);
|
|
57
|
+
rowIds.forEach((id) => {
|
|
58
|
+
if (checked) {
|
|
59
|
+
next.add(id);
|
|
60
|
+
} else {
|
|
61
|
+
next.delete(id);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return next;
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="rounded-md border">
|
|
70
|
+
{selectable && (
|
|
71
|
+
<div className="flex flex-col gap-2 border-b px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
|
|
72
|
+
<p className="text-sm text-muted-foreground">
|
|
73
|
+
{selectedCount} {selectedCount === 1 ? "row" : "rows"} selected
|
|
74
|
+
</p>
|
|
75
|
+
<div className="flex flex-wrap gap-2">
|
|
76
|
+
<CollectionToolbarActions
|
|
77
|
+
actions={safeBulkActions}
|
|
78
|
+
selectedIds={selectedRowIds}
|
|
79
|
+
disabled={selectedCount === 0}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
<Table>
|
|
85
|
+
<TableHeader>
|
|
86
|
+
<TableRow>
|
|
87
|
+
{selectable && (
|
|
88
|
+
<TableHead className="w-10">
|
|
89
|
+
<input
|
|
90
|
+
type="checkbox"
|
|
91
|
+
aria-label="Select all rows"
|
|
92
|
+
checked={allSelected}
|
|
93
|
+
ref={(element) => {
|
|
94
|
+
if (element) element.indeterminate = someSelected;
|
|
95
|
+
}}
|
|
96
|
+
onChange={(event) => toggleAllRows(event.target.checked)}
|
|
97
|
+
className="h-4 w-4 rounded border"
|
|
98
|
+
/>
|
|
99
|
+
</TableHead>
|
|
100
|
+
)}
|
|
101
|
+
{safeHeaders.map((header) => (
|
|
102
|
+
<SortableHeader key={header.attribute} {...header} />
|
|
103
|
+
))}
|
|
104
|
+
{showActions && (
|
|
105
|
+
<TableHead className="w-10">
|
|
106
|
+
<span className="sr-only">Actions</span>
|
|
107
|
+
</TableHead>
|
|
108
|
+
)}
|
|
109
|
+
</TableRow>
|
|
110
|
+
</TableHeader>
|
|
111
|
+
<TableBody>
|
|
112
|
+
{safeRows.length === 0 ? (
|
|
113
|
+
<TableRow>
|
|
114
|
+
<TableCell colSpan={columnCount} className="h-32 text-center">
|
|
115
|
+
<div className="mx-auto flex max-w-sm flex-col items-center gap-1">
|
|
116
|
+
<p className="text-sm font-medium">{emptyState?.title ?? "No records found"}</p>
|
|
117
|
+
{emptyState?.description && (
|
|
118
|
+
<p className="text-sm text-muted-foreground">{emptyState.description}</p>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</TableCell>
|
|
122
|
+
</TableRow>
|
|
123
|
+
) : safeRows.map((row) => (
|
|
124
|
+
<TableRow
|
|
125
|
+
key={row.id}
|
|
126
|
+
className={cn(
|
|
127
|
+
row.showPath && "cursor-pointer",
|
|
128
|
+
row.rowOptions?.className || row.rowOptions?.class_name
|
|
129
|
+
)}
|
|
130
|
+
onClick={(e) => handleRowClick(e, row.showPath)}
|
|
131
|
+
>
|
|
132
|
+
{selectable && (
|
|
133
|
+
<TableCell>
|
|
134
|
+
<input
|
|
135
|
+
type="checkbox"
|
|
136
|
+
aria-label={`Select row ${row.id}`}
|
|
137
|
+
checked={selectedIds.has(String(row.id))}
|
|
138
|
+
onChange={(event) => toggleRow(String(row.id), event.target.checked)}
|
|
139
|
+
className="h-4 w-4 rounded border"
|
|
140
|
+
/>
|
|
141
|
+
</TableCell>
|
|
142
|
+
)}
|
|
143
|
+
{(Array.isArray(row.cells) ? row.cells : []).map((cell) => {
|
|
144
|
+
const cellClassName = cell.cellOptions?.className || cell.cellOptions?.class_name;
|
|
145
|
+
return (
|
|
146
|
+
<TableCell key={cell.attribute} className={cellClassName}>
|
|
147
|
+
{cell.showPath ? (
|
|
148
|
+
<a
|
|
149
|
+
href={cell.showPath}
|
|
150
|
+
data-sg-visit
|
|
151
|
+
className="hover:underline"
|
|
152
|
+
>
|
|
153
|
+
<FieldRenderer mode="index" {...cell} />
|
|
154
|
+
</a>
|
|
155
|
+
) : (
|
|
156
|
+
<FieldRenderer mode="index" {...cell} />
|
|
157
|
+
)}
|
|
158
|
+
</TableCell>
|
|
159
|
+
);
|
|
160
|
+
})}
|
|
161
|
+
{showActions && (
|
|
162
|
+
<TableCell>
|
|
163
|
+
<CollectionItemActions actions={row.collectionItemActions} />
|
|
164
|
+
</TableCell>
|
|
165
|
+
)}
|
|
166
|
+
</TableRow>
|
|
167
|
+
))}
|
|
168
|
+
</TableBody>
|
|
169
|
+
</Table>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import React, { useRef, useContext } from "react";
|
|
2
2
|
import { NavigationContext } from "@thoughtbot/superglue";
|
|
3
3
|
|
|
4
|
-
import { Input } from "
|
|
4
|
+
import { Input } from "./ui";
|
|
5
5
|
import { Search } from "lucide-react";
|
|
6
6
|
|
|
7
|
-
export function SearchBar({ searchTerm, searchPath }) {
|
|
7
|
+
export function SearchBar({ searchTerm, searchPath, perPage }) {
|
|
8
8
|
const { visit } = useContext(NavigationContext);
|
|
9
9
|
const inputRef = useRef(null);
|
|
10
10
|
|
|
11
11
|
const handleSubmit = (e) => {
|
|
12
12
|
e.preventDefault();
|
|
13
13
|
const search = inputRef.current?.value ?? "";
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
const url = new URL(window.location.href);
|
|
15
|
+
url.pathname = searchPath;
|
|
16
|
+
url.searchParams.delete("_page");
|
|
17
|
+
if (perPage != null) url.searchParams.set("per_page", String(perPage));
|
|
18
|
+
|
|
19
|
+
if (search.trim()) {
|
|
20
|
+
url.searchParams.set("search", search);
|
|
21
|
+
} else {
|
|
22
|
+
url.searchParams.delete("search");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
visit(url.pathname + url.search, {});
|
|
18
26
|
};
|
|
19
27
|
|
|
20
28
|
return (
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react";
|
|
3
3
|
|
|
4
|
-
import { TableHead } from "
|
|
4
|
+
import { TableHead } from "./ui";
|
|
5
|
+
|
|
6
|
+
export function SortableHeader({ label, sortable, sortUrl, sortDirection, headerOptions }) {
|
|
7
|
+
const headerClassName = headerOptions?.className || headerOptions?.class_name;
|
|
5
8
|
|
|
6
|
-
export function SortableHeader({ label, sortable, sortUrl, sortDirection }) {
|
|
7
9
|
if (!sortable) {
|
|
8
|
-
return <TableHead>{label}</TableHead>;
|
|
10
|
+
return <TableHead className={headerClassName}>{label}</TableHead>;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
return (
|
|
12
|
-
<TableHead>
|
|
14
|
+
<TableHead className={headerClassName}>
|
|
13
15
|
<a
|
|
14
16
|
href={sortUrl}
|
|
15
|
-
data-sg-
|
|
17
|
+
data-sg-visit
|
|
16
18
|
className="inline-flex items-center gap-1 hover:text-foreground">
|
|
17
19
|
|
|
18
20
|
{label}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { Label, Input } from "../../components/ui";
|
|
4
|
+
|
|
5
|
+
export function FormField({ value, label, input, required }) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="space-y-2">
|
|
8
|
+
{label && <Label htmlFor={input?.id}>{label}{required && <span> *</span>}</Label>}
|
|
9
|
+
{value?.filename && (
|
|
10
|
+
<p className="text-sm text-muted-foreground">Current: {value.filename}</p>
|
|
11
|
+
)}
|
|
12
|
+
<Input type="file" {...input} />
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
function formatBytes(bytes) {
|
|
4
|
+
if (bytes == null || bytes === 0) return "0 B";
|
|
5
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
6
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
7
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ShowField({ value }) {
|
|
11
|
+
if (!value) return <span className="text-muted-foreground">No file</span>;
|
|
12
|
+
return <span>{value.filename} ({formatBytes(value.byteSize)})</span>;
|
|
13
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React, { useState, useRef, useCallback } from "react";
|
|
2
2
|
import { X, Check } from "lucide-react";
|
|
3
3
|
|
|
4
|
-
import { Label } from "
|
|
5
|
-
import { Badge } from "
|
|
6
|
-
import { Popover, PopoverTrigger, PopoverContent } from "
|
|
4
|
+
import { Label } from "../../components/ui";
|
|
5
|
+
import { Badge } from "../../components/ui";
|
|
6
|
+
import { Popover, PopoverTrigger, PopoverContent } from "../../components/ui";
|
|
7
7
|
import { cn } from "terrazzo";
|
|
8
8
|
|
|
9
9
|
export function FormField({ value, label, input, options, required }) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
import { Badge } from "
|
|
3
|
+
import { Badge } from "../../components/ui";
|
|
4
4
|
|
|
5
5
|
export function IndexField({ value }) {
|
|
6
|
-
const count =
|
|
7
|
-
|
|
6
|
+
const count = value?.count ?? 0;
|
|
7
|
+
const label = value?.label ?? "";
|
|
8
|
+
return <Badge variant="secondary">{count} {label}</Badge>;
|
|
8
9
|
}
|
|
@@ -1,25 +1,30 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from "react";
|
|
2
2
|
|
|
3
|
-
import { ResourceTable } from "
|
|
4
|
-
import { Badge } from "
|
|
5
|
-
import { Button } from "terrazzo/ui";
|
|
3
|
+
import { ResourceTable, HasManyPagination } from "../../components";
|
|
4
|
+
import { Badge } from "../../components/ui";
|
|
6
5
|
|
|
7
|
-
export function ShowField({ value, hasManyRowExtras, options }) {
|
|
6
|
+
export function ShowField({ value, hasManyRowExtras, paginationPaths, options }) {
|
|
8
7
|
if (!value) return <span className="text-muted-foreground">None</span>;
|
|
9
8
|
|
|
10
|
-
const { rows, headers, total,
|
|
11
|
-
|
|
9
|
+
const { rows, headers, items, total, currentPage, totalPages } = value;
|
|
10
|
+
|
|
11
|
+
const pagination = (
|
|
12
|
+
<HasManyPagination
|
|
13
|
+
currentPage={currentPage}
|
|
14
|
+
totalPages={totalPages}
|
|
15
|
+
total={total}
|
|
16
|
+
nextPagePath={paginationPaths?.nextPagePath}
|
|
17
|
+
prevPagePath={paginationPaths?.prevPagePath}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
12
20
|
|
|
13
21
|
// Table mode: collection_attributes specified
|
|
14
22
|
if (headers && rows) {
|
|
15
|
-
if (rows.length === 0) {
|
|
23
|
+
if (rows.length === 0 && total === 0) {
|
|
16
24
|
return <span className="text-muted-foreground">None</span>;
|
|
17
25
|
}
|
|
18
26
|
|
|
19
|
-
const
|
|
20
|
-
const visibleRows = expanded || !hasMore ? rows : rows.slice(0, initialLimit);
|
|
21
|
-
|
|
22
|
-
const enrichedRows = visibleRows.map((row) => {
|
|
27
|
+
const enrichedRows = rows.map((row) => {
|
|
23
28
|
const extras = hasManyRowExtras?.[String(row.id)] || {};
|
|
24
29
|
return {
|
|
25
30
|
...row,
|
|
@@ -31,31 +36,20 @@ export function ShowField({ value, hasManyRowExtras, options }) {
|
|
|
31
36
|
return (
|
|
32
37
|
<div>
|
|
33
38
|
<ResourceTable headers={headers} rows={enrichedRows} showActions={options?.renderActions !== false} />
|
|
34
|
-
{
|
|
35
|
-
<Button
|
|
36
|
-
variant="link"
|
|
37
|
-
size="sm"
|
|
38
|
-
className="mt-2 px-0"
|
|
39
|
-
onClick={() => setExpanded(!expanded)}>
|
|
40
|
-
{expanded ? "Show less" : `Show ${total - initialLimit} more`}
|
|
41
|
-
</Button>
|
|
42
|
-
)}
|
|
39
|
+
{pagination}
|
|
43
40
|
</div>
|
|
44
41
|
);
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
// Simple list mode (no collection_attributes)
|
|
48
|
-
if (!items || items.length === 0) {
|
|
45
|
+
if ((!items || items.length === 0) && total === 0) {
|
|
49
46
|
return <span className="text-muted-foreground">None</span>;
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
const hasMore = initialLimit && initialLimit > 0 && total > initialLimit;
|
|
53
|
-
const visibleItems = expanded || !hasMore ? items : items.slice(0, initialLimit);
|
|
54
|
-
|
|
55
49
|
return (
|
|
56
50
|
<div>
|
|
57
51
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
58
|
-
{
|
|
52
|
+
{(items || []).map((item) => {
|
|
59
53
|
const showPath = hasManyRowExtras?.[String(item.id)]?.showPath;
|
|
60
54
|
return showPath ? (
|
|
61
55
|
<a key={item.id} href={showPath} data-sg-visit>
|
|
@@ -65,16 +59,8 @@ export function ShowField({ value, hasManyRowExtras, options }) {
|
|
|
65
59
|
<Badge key={item.id} variant="secondary">{item.display}</Badge>
|
|
66
60
|
);
|
|
67
61
|
})}
|
|
68
|
-
{hasMore && (
|
|
69
|
-
<Button
|
|
70
|
-
variant="link"
|
|
71
|
-
size="sm"
|
|
72
|
-
className="px-0"
|
|
73
|
-
onClick={() => setExpanded(!expanded)}>
|
|
74
|
-
{expanded ? "Show less" : `Show ${total - initialLimit} more`}
|
|
75
|
-
</Button>
|
|
76
|
-
)}
|
|
77
62
|
</div>
|
|
63
|
+
{pagination}
|
|
78
64
|
</div>
|
|
79
65
|
);
|
|
80
66
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import React, { useState, useRef
|
|
1
|
+
import React, { useState, useRef } from "react";
|
|
2
2
|
|
|
3
|
-
import { Input } from "
|
|
4
|
-
import { Button } from "
|
|
5
|
-
import { Label } from "
|
|
3
|
+
import { Input } from "../../components/ui";
|
|
4
|
+
import { Button } from "../../components/ui";
|
|
5
|
+
import { Label } from "../../components/ui";
|
|
6
6
|
|
|
7
|
-
export function FormField({ value, label, input,
|
|
7
|
+
export function FormField({ value, label, input, required }) {
|
|
8
8
|
const initialPairs = () => {
|
|
9
9
|
if (value && typeof value === "object" && Object.keys(value).length > 0) {
|
|
10
10
|
return Object.entries(value).map(([k, v], i) => ({ id: i, key: k, value: v ?? "" }));
|
|
@@ -5,5 +5,5 @@ export function IndexField({ value, options }) {
|
|
|
5
5
|
const suffix = options?.suffix ?? "";
|
|
6
6
|
const decimals = options?.decimals;
|
|
7
7
|
const formatted = decimals != null && value != null ? Number(value).toFixed(decimals) : String(value ?? "");
|
|
8
|
-
return <span className="text-sm tabular-nums">{prefix}{formatted}{suffix}</span>;
|
|
8
|
+
return <span className="text-sm tabular-nums">{prefix}{formatted}{suffix && ` ${suffix}`}</span>;
|
|
9
9
|
}
|
|
@@ -5,5 +5,5 @@ export function ShowField({ value, options }) {
|
|
|
5
5
|
const suffix = options?.suffix ?? "";
|
|
6
6
|
const decimals = options?.decimals;
|
|
7
7
|
const formatted = decimals != null && value != null ? Number(value).toFixed(decimals) : String(value ?? "");
|
|
8
|
-
return <span className="tabular-nums">{prefix}{formatted}{suffix}</span>;
|
|
8
|
+
return <span className="tabular-nums">{prefix}{formatted}{suffix && ` ${suffix}`}</span>;
|
|
9
9
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
2
|
|
|
3
|
-
import { Label } from "
|
|
3
|
+
import { Label } from "../../components/ui";
|
|
4
4
|
|
|
5
5
|
export function FormField({ value, label, input, options, required }) {
|
|
6
6
|
const groupedOptions = options?.groupedOptions ?? {};
|
|
@@ -13,7 +13,8 @@ export function FormField({ value, label, input, options, required }) {
|
|
|
13
13
|
|
|
14
14
|
const filteredItems = selectedType ? groupedOptions[selectedType] ?? [] : [];
|
|
15
15
|
|
|
16
|
-
const
|
|
16
|
+
const idInputProps = { ...(inputProps ?? {}) };
|
|
17
|
+
delete idInputProps.typeName;
|
|
17
18
|
|
|
18
19
|
return (
|
|
19
20
|
<div className="space-y-2">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
import { Textarea } from "
|
|
4
|
-
import { Label } from "
|
|
3
|
+
import { Textarea } from "../../components/ui";
|
|
4
|
+
import { Label } from "../../components/ui";
|
|
5
5
|
|
|
6
6
|
export function FormField({ value, label, input, required }) {
|
|
7
7
|
return (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
import { Input } from "
|
|
4
|
-
import { Label } from "
|
|
3
|
+
import { Input } from "../../components/ui";
|
|
4
|
+
import { Label } from "../../components/ui";
|
|
5
5
|
|
|
6
6
|
export function TextInputFormField({ type = "text", value, label, input, required, defaultValue, ...inputProps }) {
|
|
7
7
|
const resolvedDefault = defaultValue !== undefined ? defaultValue : String(value ?? "");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
import { Textarea } from "
|
|
4
|
-
import { Label } from "
|
|
3
|
+
import { Textarea } from "../../components/ui";
|
|
4
|
+
import { Label } from "../../components/ui";
|
|
5
5
|
|
|
6
6
|
export function FormField({ value, label, input, required }) {
|
|
7
7
|
return (
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
import { ResourceTable } from "
|
|
3
|
+
import { ResourceTable } from "../components";
|
|
4
|
+
|
|
5
|
+
export function AdminCollection({ table = {}, emptyState, bulkActions = [] }) {
|
|
6
|
+
const { headers = [], rows = [] } = table || {};
|
|
4
7
|
|
|
5
|
-
export function AdminCollection({ table }) {
|
|
6
8
|
return (
|
|
7
9
|
<div className="overflow-x-auto">
|
|
8
|
-
<ResourceTable headers={
|
|
10
|
+
<ResourceTable headers={headers} rows={rows} emptyState={emptyState} bulkActions={bulkActions} />
|
|
9
11
|
</div>
|
|
10
12
|
);
|
|
11
13
|
}
|