terrazzo 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/Rakefile +11 -0
- data/app/controllers/terrazzo/application_controller.rb +208 -0
- data/app/views/terrazzo/application/edit.json.props +70 -0
- data/app/views/terrazzo/application/index.json.props +97 -0
- data/app/views/terrazzo/application/new.json.props +65 -0
- data/app/views/terrazzo/application/show.json.props +44 -0
- data/app/views/terrazzo/application/superglue.html.erb +5 -0
- data/config/locales/en.yml +28 -0
- data/lib/generators/terrazzo/dashboard/dashboard_generator.rb +118 -0
- data/lib/generators/terrazzo/dashboard/templates/controller.rb.erb +4 -0
- data/lib/generators/terrazzo/dashboard/templates/dashboard.rb.erb +36 -0
- data/lib/generators/terrazzo/field/field_generator.rb +42 -0
- data/lib/generators/terrazzo/field/templates/FormField.jsx.erb +19 -0
- data/lib/generators/terrazzo/field/templates/IndexField.jsx.erb +5 -0
- data/lib/generators/terrazzo/field/templates/ShowField.jsx.erb +5 -0
- data/lib/generators/terrazzo/field/templates/field.rb.erb +23 -0
- data/lib/generators/terrazzo/install/install_generator.rb +85 -0
- data/lib/generators/terrazzo/install/templates/application.js.erb +27 -0
- data/lib/generators/terrazzo/install/templates/application.json.props +27 -0
- data/lib/generators/terrazzo/install/templates/application.json.props.erb +17 -0
- data/lib/generators/terrazzo/install/templates/application_controller.rb.erb +24 -0
- data/lib/generators/terrazzo/install/templates/application_visit.js.erb +8 -0
- data/lib/generators/terrazzo/install/templates/flash_slice.js.erb +42 -0
- data/lib/generators/terrazzo/install/templates/page_to_page_mapping.js.erb +22 -0
- data/lib/generators/terrazzo/install/templates/store.js.erb +24 -0
- data/lib/generators/terrazzo/install/templates/superglue.html.erb.erb +20 -0
- data/lib/generators/terrazzo/routes/routes_generator.rb +38 -0
- data/lib/generators/terrazzo/views/edit_generator.rb +28 -0
- data/lib/generators/terrazzo/views/field_generator.rb +32 -0
- data/lib/generators/terrazzo/views/index_generator.rb +24 -0
- data/lib/generators/terrazzo/views/layout_generator.rb +26 -0
- data/lib/generators/terrazzo/views/navigation_generator.rb +24 -0
- data/lib/generators/terrazzo/views/new_generator.rb +28 -0
- data/lib/generators/terrazzo/views/show_generator.rb +24 -0
- data/lib/generators/terrazzo/views/templates/components/FlashMessages.jsx +26 -0
- data/lib/generators/terrazzo/views/templates/components/Layout.jsx +23 -0
- data/lib/generators/terrazzo/views/templates/components/Pagination.jsx +69 -0
- data/lib/generators/terrazzo/views/templates/components/SearchBar.jsx +35 -0
- data/lib/generators/terrazzo/views/templates/components/SortableHeader.jsx +29 -0
- data/lib/generators/terrazzo/views/templates/components/app-sidebar.jsx +62 -0
- data/lib/generators/terrazzo/views/templates/components/site-header.jsx +19 -0
- data/lib/generators/terrazzo/views/templates/components/ui/avatar.jsx +35 -0
- data/lib/generators/terrazzo/views/templates/components/ui/badge.jsx +34 -0
- data/lib/generators/terrazzo/views/templates/components/ui/button.jsx +47 -0
- data/lib/generators/terrazzo/views/templates/components/ui/card.jsx +50 -0
- data/lib/generators/terrazzo/views/templates/components/ui/dropdown-menu.jsx +155 -0
- data/lib/generators/terrazzo/views/templates/components/ui/field.jsx +28 -0
- data/lib/generators/terrazzo/views/templates/components/ui/index.js +106 -0
- data/lib/generators/terrazzo/views/templates/components/ui/input.jsx +19 -0
- data/lib/generators/terrazzo/views/templates/components/ui/label.jsx +16 -0
- data/lib/generators/terrazzo/views/templates/components/ui/pagination.jsx +85 -0
- data/lib/generators/terrazzo/views/templates/components/ui/popover.jsx +27 -0
- data/lib/generators/terrazzo/views/templates/components/ui/select.jsx +127 -0
- data/lib/generators/terrazzo/views/templates/components/ui/separator.jsx +23 -0
- data/lib/generators/terrazzo/views/templates/components/ui/sheet.jsx +109 -0
- data/lib/generators/terrazzo/views/templates/components/ui/sidebar.jsx +629 -0
- data/lib/generators/terrazzo/views/templates/components/ui/skeleton.jsx +10 -0
- data/lib/generators/terrazzo/views/templates/components/ui/table.jsx +83 -0
- data/lib/generators/terrazzo/views/templates/components/ui/textarea.jsx +18 -0
- data/lib/generators/terrazzo/views/templates/components/ui/tooltip.jsx +24 -0
- data/lib/generators/terrazzo/views/templates/fields/FieldRenderer.jsx +103 -0
- data/lib/generators/terrazzo/views/templates/fields/belongs_to/FormField.jsx +29 -0
- data/lib/generators/terrazzo/views/templates/fields/belongs_to/IndexField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/belongs_to/ShowField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/boolean/FormField.jsx +21 -0
- data/lib/generators/terrazzo/views/templates/fields/boolean/IndexField.jsx +12 -0
- data/lib/generators/terrazzo/views/templates/fields/boolean/ShowField.jsx +6 -0
- data/lib/generators/terrazzo/views/templates/fields/date/FormField.jsx +8 -0
- data/lib/generators/terrazzo/views/templates/fields/date/IndexField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/date/ShowField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/date_time/FormField.jsx +8 -0
- data/lib/generators/terrazzo/views/templates/fields/date_time/IndexField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/date_time/ShowField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/email/FormField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/email/IndexField.jsx +10 -0
- data/lib/generators/terrazzo/views/templates/fields/email/ShowField.jsx +10 -0
- data/lib/generators/terrazzo/views/templates/fields/has_many/FormField.jsx +151 -0
- data/lib/generators/terrazzo/views/templates/fields/has_many/IndexField.jsx +8 -0
- data/lib/generators/terrazzo/views/templates/fields/has_many/ShowField.jsx +72 -0
- data/lib/generators/terrazzo/views/templates/fields/has_one/FormField.jsx +28 -0
- data/lib/generators/terrazzo/views/templates/fields/has_one/IndexField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/has_one/ShowField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/hstore/FormField.jsx +120 -0
- data/lib/generators/terrazzo/views/templates/fields/hstore/IndexField.jsx +15 -0
- data/lib/generators/terrazzo/views/templates/fields/hstore/ShowField.jsx +24 -0
- data/lib/generators/terrazzo/views/templates/fields/index.js +81 -0
- data/lib/generators/terrazzo/views/templates/fields/number/FormField.jsx +9 -0
- data/lib/generators/terrazzo/views/templates/fields/number/IndexField.jsx +9 -0
- data/lib/generators/terrazzo/views/templates/fields/number/ShowField.jsx +9 -0
- data/lib/generators/terrazzo/views/templates/fields/password/FormField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/password/IndexField.jsx +6 -0
- data/lib/generators/terrazzo/views/templates/fields/password/ShowField.jsx +6 -0
- data/lib/generators/terrazzo/views/templates/fields/polymorphic/FormField.jsx +58 -0
- data/lib/generators/terrazzo/views/templates/fields/polymorphic/IndexField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/polymorphic/ShowField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/rich_text/FormField.jsx +21 -0
- data/lib/generators/terrazzo/views/templates/fields/rich_text/IndexField.jsx +8 -0
- data/lib/generators/terrazzo/views/templates/fields/rich_text/ShowField.jsx +6 -0
- data/lib/generators/terrazzo/views/templates/fields/select/FormField.jsx +29 -0
- data/lib/generators/terrazzo/views/templates/fields/select/IndexField.jsx +8 -0
- data/lib/generators/terrazzo/views/templates/fields/select/ShowField.jsx +5 -0
- data/lib/generators/terrazzo/views/templates/fields/shared/TextInputFormField.jsx +24 -0
- data/lib/generators/terrazzo/views/templates/fields/string/FormField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/string/IndexField.jsx +5 -0
- data/lib/generators/terrazzo/views/templates/fields/string/ShowField.jsx +5 -0
- data/lib/generators/terrazzo/views/templates/fields/text/FormField.jsx +20 -0
- data/lib/generators/terrazzo/views/templates/fields/text/IndexField.jsx +5 -0
- data/lib/generators/terrazzo/views/templates/fields/text/ShowField.jsx +5 -0
- data/lib/generators/terrazzo/views/templates/fields/time/FormField.jsx +8 -0
- data/lib/generators/terrazzo/views/templates/fields/time/IndexField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/time/ShowField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/url/FormField.jsx +7 -0
- data/lib/generators/terrazzo/views/templates/fields/url/IndexField.jsx +10 -0
- data/lib/generators/terrazzo/views/templates/fields/url/ShowField.jsx +10 -0
- data/lib/generators/terrazzo/views/templates/pages/_form.jsx +76 -0
- data/lib/generators/terrazzo/views/templates/pages/edit.jsx +44 -0
- data/lib/generators/terrazzo/views/templates/pages/index.jsx +106 -0
- data/lib/generators/terrazzo/views/templates/pages/new.jsx +36 -0
- data/lib/generators/terrazzo/views/templates/pages/show.jsx +82 -0
- data/lib/generators/terrazzo/views/views_generator.rb +52 -0
- data/lib/terrazzo/base_dashboard.rb +88 -0
- data/lib/terrazzo/engine.rb +21 -0
- data/lib/terrazzo/field/associative.rb +56 -0
- data/lib/terrazzo/field/base.rb +114 -0
- data/lib/terrazzo/field/belongs_to.rb +53 -0
- data/lib/terrazzo/field/boolean.rb +9 -0
- data/lib/terrazzo/field/date.rb +10 -0
- data/lib/terrazzo/field/date_time.rb +10 -0
- data/lib/terrazzo/field/deferred.rb +50 -0
- data/lib/terrazzo/field/email.rb +15 -0
- data/lib/terrazzo/field/has_many.rb +98 -0
- data/lib/terrazzo/field/has_one.rb +33 -0
- data/lib/terrazzo/field/hstore.rb +37 -0
- data/lib/terrazzo/field/money.rb +33 -0
- data/lib/terrazzo/field/number.rb +17 -0
- data/lib/terrazzo/field/password.rb +16 -0
- data/lib/terrazzo/field/polymorphic.rb +36 -0
- data/lib/terrazzo/field/rich_text.rb +30 -0
- data/lib/terrazzo/field/select.rb +33 -0
- data/lib/terrazzo/field/string.rb +27 -0
- data/lib/terrazzo/field/text.rb +27 -0
- data/lib/terrazzo/field/time.rb +10 -0
- data/lib/terrazzo/field/url.rb +9 -0
- data/lib/terrazzo/filter.rb +26 -0
- data/lib/terrazzo/generator_helpers.rb +36 -0
- data/lib/terrazzo/namespace/resource.rb +39 -0
- data/lib/terrazzo/namespace.rb +34 -0
- data/lib/terrazzo/not_authorized_error.rb +4 -0
- data/lib/terrazzo/order.rb +71 -0
- data/lib/terrazzo/page/base.rb +12 -0
- data/lib/terrazzo/page/collection.rb +28 -0
- data/lib/terrazzo/page/form.rb +43 -0
- data/lib/terrazzo/page/show.rb +46 -0
- data/lib/terrazzo/resource_resolver.rb +40 -0
- data/lib/terrazzo/search.rb +56 -0
- data/lib/terrazzo/version.rb +3 -0
- data/lib/terrazzo.rb +47 -0
- data/terrazzo.gemspec +32 -0
- metadata +297 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "terrazzo"
|
|
4
|
+
|
|
5
|
+
const Table = React.forwardRef(({ className, ...props }, ref) => (
|
|
6
|
+
<div className="relative w-full overflow-auto">
|
|
7
|
+
<table
|
|
8
|
+
ref={ref}
|
|
9
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
10
|
+
{...props} />
|
|
11
|
+
</div>
|
|
12
|
+
))
|
|
13
|
+
Table.displayName = "Table"
|
|
14
|
+
|
|
15
|
+
const TableHeader = React.forwardRef(({ className, ...props }, ref) => (
|
|
16
|
+
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
|
17
|
+
))
|
|
18
|
+
TableHeader.displayName = "TableHeader"
|
|
19
|
+
|
|
20
|
+
const TableBody = React.forwardRef(({ className, ...props }, ref) => (
|
|
21
|
+
<tbody
|
|
22
|
+
ref={ref}
|
|
23
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
24
|
+
{...props} />
|
|
25
|
+
))
|
|
26
|
+
TableBody.displayName = "TableBody"
|
|
27
|
+
|
|
28
|
+
const TableFooter = React.forwardRef(({ className, ...props }, ref) => (
|
|
29
|
+
<tfoot
|
|
30
|
+
ref={ref}
|
|
31
|
+
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
|
32
|
+
{...props} />
|
|
33
|
+
))
|
|
34
|
+
TableFooter.displayName = "TableFooter"
|
|
35
|
+
|
|
36
|
+
const TableRow = React.forwardRef(({ className, ...props }, ref) => (
|
|
37
|
+
<tr
|
|
38
|
+
ref={ref}
|
|
39
|
+
className={cn(
|
|
40
|
+
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
{...props} />
|
|
44
|
+
))
|
|
45
|
+
TableRow.displayName = "TableRow"
|
|
46
|
+
|
|
47
|
+
const TableHead = React.forwardRef(({ className, ...props }, ref) => (
|
|
48
|
+
<th
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn(
|
|
51
|
+
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
{...props} />
|
|
55
|
+
))
|
|
56
|
+
TableHead.displayName = "TableHead"
|
|
57
|
+
|
|
58
|
+
const TableCell = React.forwardRef(({ className, ...props }, ref) => (
|
|
59
|
+
<td
|
|
60
|
+
ref={ref}
|
|
61
|
+
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
|
62
|
+
{...props} />
|
|
63
|
+
))
|
|
64
|
+
TableCell.displayName = "TableCell"
|
|
65
|
+
|
|
66
|
+
const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
|
|
67
|
+
<caption
|
|
68
|
+
ref={ref}
|
|
69
|
+
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
70
|
+
{...props} />
|
|
71
|
+
))
|
|
72
|
+
TableCaption.displayName = "TableCaption"
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
Table,
|
|
76
|
+
TableHeader,
|
|
77
|
+
TableBody,
|
|
78
|
+
TableFooter,
|
|
79
|
+
TableHead,
|
|
80
|
+
TableRow,
|
|
81
|
+
TableCell,
|
|
82
|
+
TableCaption,
|
|
83
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "terrazzo"
|
|
4
|
+
|
|
5
|
+
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
|
|
6
|
+
return (
|
|
7
|
+
<textarea
|
|
8
|
+
className={cn(
|
|
9
|
+
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
10
|
+
className
|
|
11
|
+
)}
|
|
12
|
+
ref={ref}
|
|
13
|
+
{...props} />
|
|
14
|
+
);
|
|
15
|
+
})
|
|
16
|
+
Textarea.displayName = "Textarea"
|
|
17
|
+
|
|
18
|
+
export { Textarea }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
|
3
|
+
|
|
4
|
+
import { cn } from "terrazzo"
|
|
5
|
+
|
|
6
|
+
const TooltipProvider = TooltipPrimitive.Provider
|
|
7
|
+
|
|
8
|
+
const Tooltip = TooltipPrimitive.Root
|
|
9
|
+
|
|
10
|
+
const TooltipTrigger = TooltipPrimitive.Trigger
|
|
11
|
+
|
|
12
|
+
const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
|
|
13
|
+
<TooltipPrimitive.Content
|
|
14
|
+
ref={ref}
|
|
15
|
+
sideOffset={sideOffset}
|
|
16
|
+
className={cn(
|
|
17
|
+
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props} />
|
|
21
|
+
))
|
|
22
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
|
23
|
+
|
|
24
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { IndexField as StringIndex } from "./string/IndexField";
|
|
4
|
+
import { ShowField as StringShow } from "./string/ShowField";
|
|
5
|
+
import { FormField as StringForm } from "./string/FormField";
|
|
6
|
+
|
|
7
|
+
import { IndexField as TextIndex } from "./text/IndexField";
|
|
8
|
+
import { ShowField as TextShow } from "./text/ShowField";
|
|
9
|
+
import { FormField as TextForm } from "./text/FormField";
|
|
10
|
+
|
|
11
|
+
import { IndexField as NumberIndex } from "./number/IndexField";
|
|
12
|
+
import { ShowField as NumberShow } from "./number/ShowField";
|
|
13
|
+
import { FormField as NumberForm } from "./number/FormField";
|
|
14
|
+
|
|
15
|
+
import { IndexField as BooleanIndex } from "./boolean/IndexField";
|
|
16
|
+
import { ShowField as BooleanShow } from "./boolean/ShowField";
|
|
17
|
+
import { FormField as BooleanForm } from "./boolean/FormField";
|
|
18
|
+
|
|
19
|
+
import { IndexField as DateIndex } from "./date/IndexField";
|
|
20
|
+
import { ShowField as DateShow } from "./date/ShowField";
|
|
21
|
+
import { FormField as DateForm } from "./date/FormField";
|
|
22
|
+
|
|
23
|
+
import { IndexField as DateTimeIndex } from "./date_time/IndexField";
|
|
24
|
+
import { ShowField as DateTimeShow } from "./date_time/ShowField";
|
|
25
|
+
import { FormField as DateTimeForm } from "./date_time/FormField";
|
|
26
|
+
|
|
27
|
+
import { IndexField as TimeIndex } from "./time/IndexField";
|
|
28
|
+
import { ShowField as TimeShow } from "./time/ShowField";
|
|
29
|
+
import { FormField as TimeForm } from "./time/FormField";
|
|
30
|
+
|
|
31
|
+
import { IndexField as EmailIndex } from "./email/IndexField";
|
|
32
|
+
import { ShowField as EmailShow } from "./email/ShowField";
|
|
33
|
+
import { FormField as EmailForm } from "./email/FormField";
|
|
34
|
+
|
|
35
|
+
import { IndexField as UrlIndex } from "./url/IndexField";
|
|
36
|
+
import { ShowField as UrlShow } from "./url/ShowField";
|
|
37
|
+
import { FormField as UrlForm } from "./url/FormField";
|
|
38
|
+
|
|
39
|
+
import { IndexField as PasswordIndex } from "./password/IndexField";
|
|
40
|
+
import { ShowField as PasswordShow } from "./password/ShowField";
|
|
41
|
+
import { FormField as PasswordForm } from "./password/FormField";
|
|
42
|
+
|
|
43
|
+
import { IndexField as SelectIndex } from "./select/IndexField";
|
|
44
|
+
import { ShowField as SelectShow } from "./select/ShowField";
|
|
45
|
+
import { FormField as SelectForm } from "./select/FormField";
|
|
46
|
+
|
|
47
|
+
import { IndexField as HstoreIndex } from "./hstore/IndexField";
|
|
48
|
+
import { ShowField as HstoreShow } from "./hstore/ShowField";
|
|
49
|
+
import { FormField as HstoreForm } from "./hstore/FormField";
|
|
50
|
+
|
|
51
|
+
import { IndexField as RichTextIndex } from "./rich_text/IndexField";
|
|
52
|
+
import { ShowField as RichTextShow } from "./rich_text/ShowField";
|
|
53
|
+
import { FormField as RichTextForm } from "./rich_text/FormField";
|
|
54
|
+
|
|
55
|
+
import { IndexField as BelongsToIndex } from "./belongs_to/IndexField";
|
|
56
|
+
import { ShowField as BelongsToShow } from "./belongs_to/ShowField";
|
|
57
|
+
import { FormField as BelongsToForm } from "./belongs_to/FormField";
|
|
58
|
+
|
|
59
|
+
import { IndexField as HasManyIndex } from "./has_many/IndexField";
|
|
60
|
+
import { ShowField as HasManyShow } from "./has_many/ShowField";
|
|
61
|
+
import { FormField as HasManyForm } from "./has_many/FormField";
|
|
62
|
+
|
|
63
|
+
import { IndexField as HasOneIndex } from "./has_one/IndexField";
|
|
64
|
+
import { ShowField as HasOneShow } from "./has_one/ShowField";
|
|
65
|
+
import { FormField as HasOneForm } from "./has_one/FormField";
|
|
66
|
+
|
|
67
|
+
import { IndexField as PolymorphicIndex } from "./polymorphic/IndexField";
|
|
68
|
+
import { ShowField as PolymorphicShow } from "./polymorphic/ShowField";
|
|
69
|
+
import { FormField as PolymorphicForm } from "./polymorphic/FormField";
|
|
70
|
+
|
|
71
|
+
const fieldMap = {
|
|
72
|
+
string: { index: StringIndex, show: StringShow, form: StringForm },
|
|
73
|
+
text: { index: TextIndex, show: TextShow, form: TextForm },
|
|
74
|
+
number: { index: NumberIndex, show: NumberShow, form: NumberForm },
|
|
75
|
+
boolean: { index: BooleanIndex, show: BooleanShow, form: BooleanForm },
|
|
76
|
+
date: { index: DateIndex, show: DateShow, form: DateForm },
|
|
77
|
+
date_time: { index: DateTimeIndex, show: DateTimeShow, form: DateTimeForm },
|
|
78
|
+
time: { index: TimeIndex, show: TimeShow, form: TimeForm },
|
|
79
|
+
email: { index: EmailIndex, show: EmailShow, form: EmailForm },
|
|
80
|
+
url: { index: UrlIndex, show: UrlShow, form: UrlForm },
|
|
81
|
+
password: { index: PasswordIndex, show: PasswordShow, form: PasswordForm },
|
|
82
|
+
select: { index: SelectIndex, show: SelectShow, form: SelectForm },
|
|
83
|
+
hstore: { index: HstoreIndex, show: HstoreShow, form: HstoreForm },
|
|
84
|
+
rich_text: { index: RichTextIndex, show: RichTextShow, form: RichTextForm },
|
|
85
|
+
belongs_to: { index: BelongsToIndex, show: BelongsToShow, form: BelongsToForm },
|
|
86
|
+
has_many: { index: HasManyIndex, show: HasManyShow, form: HasManyForm },
|
|
87
|
+
has_one: { index: HasOneIndex, show: HasOneShow, form: HasOneForm },
|
|
88
|
+
polymorphic: { index: PolymorphicIndex, show: PolymorphicShow, form: PolymorphicForm }
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Allow consumers to register custom field components
|
|
92
|
+
export function registerFieldType(
|
|
93
|
+
fieldType,
|
|
94
|
+
components)
|
|
95
|
+
{
|
|
96
|
+
fieldMap[fieldType] = { ...fieldMap[fieldType], ...components };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function FieldRenderer({ mode, fieldType, ...rest }) {
|
|
100
|
+
const Component = fieldMap[fieldType]?.[mode];
|
|
101
|
+
if (!Component) return <span>{String(rest.value ?? "")}</span>;
|
|
102
|
+
return <Component {...rest} />;
|
|
103
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { Label } from "../../components/ui/label";
|
|
4
|
+
|
|
5
|
+
export function FormField({ value, label, input, options, required }) {
|
|
6
|
+
const resourceOptions = options?.resourceOptions ?? [];
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="space-y-2">
|
|
10
|
+
{label &&
|
|
11
|
+
<Label htmlFor={input?.id}>
|
|
12
|
+
{label}{required && <span className="text-destructive"> *</span>}
|
|
13
|
+
</Label>
|
|
14
|
+
}
|
|
15
|
+
<select
|
|
16
|
+
defaultValue={value != null ? String(value) : ""}
|
|
17
|
+
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
18
|
+
{...input}>
|
|
19
|
+
|
|
20
|
+
<option value="">Select...</option>
|
|
21
|
+
{resourceOptions.map(([display, id]) =>
|
|
22
|
+
<option key={id} value={id}>
|
|
23
|
+
{display}
|
|
24
|
+
</option>
|
|
25
|
+
)}
|
|
26
|
+
</select>
|
|
27
|
+
</div>);
|
|
28
|
+
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { Label } from "../../components/ui/label";
|
|
4
|
+
|
|
5
|
+
export function FormField({ value, label, input, required }) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex items-center space-x-2">
|
|
8
|
+
<input
|
|
9
|
+
type="checkbox"
|
|
10
|
+
defaultChecked={!!value}
|
|
11
|
+
className="h-4 w-4 rounded border-gray-300"
|
|
12
|
+
{...input} />
|
|
13
|
+
|
|
14
|
+
{label &&
|
|
15
|
+
<Label htmlFor={input?.id}>
|
|
16
|
+
{label}{required && <span className="text-destructive"> *</span>}
|
|
17
|
+
</Label>
|
|
18
|
+
}
|
|
19
|
+
</div>);
|
|
20
|
+
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { Badge } from "../../components/ui/badge";
|
|
4
|
+
|
|
5
|
+
export function IndexField({ value }) {
|
|
6
|
+
if (value == null) return <span className="text-muted-foreground">-</span>;
|
|
7
|
+
return (
|
|
8
|
+
<Badge variant={value ? "default" : "secondary"}>
|
|
9
|
+
{value ? "Yes" : "No"}
|
|
10
|
+
</Badge>);
|
|
11
|
+
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { TextInputFormField } from "../shared/TextInputFormField";
|
|
4
|
+
|
|
5
|
+
export function FormField({ value, ...props }) {
|
|
6
|
+
const defaultValue = value ? String(value).split("T")[0] : "";
|
|
7
|
+
return <TextInputFormField type="date" defaultValue={defaultValue} {...props} />;
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { TextInputFormField } from "../shared/TextInputFormField";
|
|
4
|
+
|
|
5
|
+
export function FormField({ value, ...props }) {
|
|
6
|
+
const defaultValue = value ? String(value).slice(0, 16) : "";
|
|
7
|
+
return <TextInputFormField type="datetime-local" defaultValue={defaultValue} {...props} />;
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export function IndexField({ value }) {
|
|
4
|
+
if (!value) return <span className="text-muted-foreground">-</span>;
|
|
5
|
+
return (
|
|
6
|
+
<a href={`mailto:${value}`} className="text-sm text-primary hover:underline">
|
|
7
|
+
{String(value)}
|
|
8
|
+
</a>);
|
|
9
|
+
|
|
10
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import React, { useState, useRef, useCallback } from "react";
|
|
2
|
+
import { X, Check } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { Label } from "../../components/ui/label";
|
|
5
|
+
import { Badge } from "../../components/ui/badge";
|
|
6
|
+
import { Popover, PopoverTrigger, PopoverContent } from "../../components/ui/popover";
|
|
7
|
+
import { cn } from "terrazzo";
|
|
8
|
+
|
|
9
|
+
export function FormField({ value, label, input, options, required }) {
|
|
10
|
+
const resourceOptions = options?.resourceOptions ?? [];
|
|
11
|
+
const initialIds = value ?? [];
|
|
12
|
+
|
|
13
|
+
const [selectedIds, setSelectedIds] = useState(() => new Set(initialIds));
|
|
14
|
+
const [search, setSearch] = useState("");
|
|
15
|
+
const [open, setOpen] = useState(false);
|
|
16
|
+
const inputRef = useRef(null);
|
|
17
|
+
|
|
18
|
+
const inputProps = input;
|
|
19
|
+
const inputName = inputProps?.name;
|
|
20
|
+
const inputId = inputProps?.id;
|
|
21
|
+
|
|
22
|
+
const toggleOption = useCallback((id) => {
|
|
23
|
+
setSelectedIds((prev) => {
|
|
24
|
+
const next = new Set(prev);
|
|
25
|
+
if (next.has(id)) {
|
|
26
|
+
next.delete(id);
|
|
27
|
+
} else {
|
|
28
|
+
next.add(id);
|
|
29
|
+
}
|
|
30
|
+
return next;
|
|
31
|
+
});
|
|
32
|
+
setSearch("");
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
const removeOption = useCallback((id, e) => {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
e.stopPropagation();
|
|
38
|
+
setSelectedIds((prev) => {
|
|
39
|
+
const next = new Set(prev);
|
|
40
|
+
next.delete(id);
|
|
41
|
+
return next;
|
|
42
|
+
});
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const selectedOptions = resourceOptions.filter(([, id]) => selectedIds.has(id));
|
|
46
|
+
const filteredOptions = resourceOptions.filter(([display]) =>
|
|
47
|
+
display.toLowerCase().includes(search.toLowerCase())
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="space-y-2">
|
|
52
|
+
{label &&
|
|
53
|
+
<Label htmlFor={inputId}>
|
|
54
|
+
{label}{required && <span className="text-destructive"> *</span>}
|
|
55
|
+
</Label>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
{/* Hidden inputs for form submission */}
|
|
59
|
+
{inputName && <input type="hidden" name={inputName} value="" />}
|
|
60
|
+
{selectedIds.size > 0 && inputName &&
|
|
61
|
+
Array.from(selectedIds).map((id) =>
|
|
62
|
+
<input key={id} type="hidden" name={inputName} value={id} />
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
67
|
+
<PopoverTrigger asChild>
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
role="combobox"
|
|
71
|
+
aria-expanded={open}
|
|
72
|
+
id={inputId}
|
|
73
|
+
className={cn(
|
|
74
|
+
"flex min-h-10 w-full flex-wrap items-center gap-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background",
|
|
75
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
76
|
+
"hover:bg-accent/50 cursor-pointer"
|
|
77
|
+
)}>
|
|
78
|
+
|
|
79
|
+
{selectedOptions.length > 0 ?
|
|
80
|
+
selectedOptions.map(([display, id]) =>
|
|
81
|
+
<Badge key={id} variant="secondary" className="gap-1">
|
|
82
|
+
{display}
|
|
83
|
+
<span
|
|
84
|
+
role="button"
|
|
85
|
+
tabIndex={0}
|
|
86
|
+
className="ml-0.5 rounded-full outline-none hover:bg-muted-foreground/20 focus:bg-muted-foreground/20"
|
|
87
|
+
onPointerDown={(e) => removeOption(id, e)}
|
|
88
|
+
onKeyDown={(e) => {
|
|
89
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
removeOption(id, e);
|
|
92
|
+
}
|
|
93
|
+
}}>
|
|
94
|
+
|
|
95
|
+
<X className="h-3 w-3" />
|
|
96
|
+
</span>
|
|
97
|
+
</Badge>
|
|
98
|
+
) :
|
|
99
|
+
|
|
100
|
+
<span className="text-muted-foreground">Select {label?.toLowerCase() ?? "items"}...</span>
|
|
101
|
+
}
|
|
102
|
+
</button>
|
|
103
|
+
</PopoverTrigger>
|
|
104
|
+
<PopoverContent
|
|
105
|
+
className="p-0"
|
|
106
|
+
onOpenAutoFocus={(e) => {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
inputRef.current?.focus();
|
|
109
|
+
}}>
|
|
110
|
+
|
|
111
|
+
<div className="p-2 border-b">
|
|
112
|
+
<input
|
|
113
|
+
ref={inputRef}
|
|
114
|
+
type="text"
|
|
115
|
+
className="flex h-8 w-full rounded-md bg-transparent px-2 py-1 text-sm outline-none placeholder:text-muted-foreground"
|
|
116
|
+
placeholder={`Search ${label?.toLowerCase() ?? "items"}...`}
|
|
117
|
+
value={search}
|
|
118
|
+
onChange={(e) => setSearch(e.target.value)} />
|
|
119
|
+
|
|
120
|
+
</div>
|
|
121
|
+
<div className="max-h-60 overflow-y-auto p-1">
|
|
122
|
+
{filteredOptions.length === 0 ?
|
|
123
|
+
<div className="px-2 py-4 text-center text-sm text-muted-foreground">
|
|
124
|
+
No results found.
|
|
125
|
+
</div> :
|
|
126
|
+
|
|
127
|
+
filteredOptions.map(([display, id]) => {
|
|
128
|
+
const isSelected = selectedIds.has(id);
|
|
129
|
+
return (
|
|
130
|
+
<button
|
|
131
|
+
key={id}
|
|
132
|
+
type="button"
|
|
133
|
+
className={cn(
|
|
134
|
+
"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm cursor-pointer",
|
|
135
|
+
"outline-none hover:bg-accent hover:text-accent-foreground",
|
|
136
|
+
"focus:bg-accent focus:text-accent-foreground"
|
|
137
|
+
)}
|
|
138
|
+
onClick={() => toggleOption(id)}>
|
|
139
|
+
|
|
140
|
+
<Check className={cn("h-4 w-4 shrink-0", isSelected ? "opacity-100" : "opacity-0")} />
|
|
141
|
+
{display}
|
|
142
|
+
</button>);
|
|
143
|
+
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
</div>
|
|
147
|
+
</PopoverContent>
|
|
148
|
+
</Popover>
|
|
149
|
+
</div>);
|
|
150
|
+
|
|
151
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Table,
|
|
5
|
+
TableHeader,
|
|
6
|
+
TableBody,
|
|
7
|
+
TableRow,
|
|
8
|
+
TableHead,
|
|
9
|
+
TableCell,
|
|
10
|
+
} from "../../components/ui/table";
|
|
11
|
+
import { FieldRenderer } from "../FieldRenderer";
|
|
12
|
+
|
|
13
|
+
export function ShowField({ value }) {
|
|
14
|
+
if (!value) return <span className="text-muted-foreground">None</span>;
|
|
15
|
+
|
|
16
|
+
const { items, headers, total, hasMore } = value;
|
|
17
|
+
|
|
18
|
+
if (!items || items.length === 0) {
|
|
19
|
+
return <span className="text-muted-foreground">None</span>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Table mode: collection_attributes specified
|
|
23
|
+
if (headers) {
|
|
24
|
+
return (
|
|
25
|
+
<div>
|
|
26
|
+
<div className="rounded-md border">
|
|
27
|
+
<Table>
|
|
28
|
+
<TableHeader>
|
|
29
|
+
<TableRow>
|
|
30
|
+
{headers.map((header) =>
|
|
31
|
+
<TableHead key={header.attribute}>{header.label}</TableHead>
|
|
32
|
+
)}
|
|
33
|
+
</TableRow>
|
|
34
|
+
</TableHeader>
|
|
35
|
+
<TableBody>
|
|
36
|
+
{items.map((item) =>
|
|
37
|
+
<TableRow key={item.id}>
|
|
38
|
+
{item.columns.map((col) =>
|
|
39
|
+
<TableCell key={col.attribute}>
|
|
40
|
+
<FieldRenderer mode="index" {...col} />
|
|
41
|
+
</TableCell>
|
|
42
|
+
)}
|
|
43
|
+
</TableRow>
|
|
44
|
+
)}
|
|
45
|
+
</TableBody>
|
|
46
|
+
</Table>
|
|
47
|
+
</div>
|
|
48
|
+
{hasMore && (
|
|
49
|
+
<p className="text-sm text-muted-foreground mt-2">
|
|
50
|
+
Showing {items.length} of {total}
|
|
51
|
+
</p>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Simple list mode
|
|
58
|
+
return (
|
|
59
|
+
<div>
|
|
60
|
+
<ul className="list-disc pl-5 space-y-1">
|
|
61
|
+
{items.map((item) =>
|
|
62
|
+
<li key={item.id}>{item.display}</li>
|
|
63
|
+
)}
|
|
64
|
+
</ul>
|
|
65
|
+
{hasMore && (
|
|
66
|
+
<p className="text-sm text-muted-foreground mt-2">
|
|
67
|
+
Showing {items.length} of {total}
|
|
68
|
+
</p>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { Label } from "../../components/ui/label";
|
|
4
|
+
|
|
5
|
+
export function FormField({ value, label, input, options, required }) {
|
|
6
|
+
const resourceOptions = options?.resourceOptions ?? [];
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="space-y-2">
|
|
10
|
+
{label &&
|
|
11
|
+
<Label htmlFor={input?.id}>
|
|
12
|
+
{label}{required && <span className="text-destructive"> *</span>}
|
|
13
|
+
</Label>
|
|
14
|
+
}
|
|
15
|
+
<select
|
|
16
|
+
defaultValue={value != null ? String(value) : ""}
|
|
17
|
+
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
18
|
+
{...input}>
|
|
19
|
+
|
|
20
|
+
<option value="">Select...</option>
|
|
21
|
+
{resourceOptions.map(([display, id]) =>
|
|
22
|
+
<option key={id} value={id}>
|
|
23
|
+
{display}
|
|
24
|
+
</option>
|
|
25
|
+
)}
|
|
26
|
+
</select>
|
|
27
|
+
</div>);
|
|
28
|
+
}
|