betterstart-cli 0.0.13 → 0.0.14
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.
- package/dist/assets/adapters/next/plugins/blog/schemas/menus.json +51 -11
- package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar.tsx +7 -1
- package/dist/assets/adapters/next/templates/init/components/layouts/sidebar-branding.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-grid.tsx +107 -0
- package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table.tsx +58 -45
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/lifecycle-hooks-tab.tsx +3 -3
- package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/snippets-tab.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/entity-filters-bar.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/media/edit-media-dialog-content.tsx +2 -2
- package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/components/shared/sort-indicator.tsx +7 -3
- package/dist/assets/adapters/next/templates/init/data/navigation.ts +2 -0
- package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-table-button.ts +85 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/get-audit-logs.ts +41 -16
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/index.ts +2 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/audit/types.ts +5 -0
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +2 -1
- package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/types.ts +2 -1
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-drawer.tsx +51 -43
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-row.tsx +2 -4
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-content.tsx +465 -179
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-skeleton.tsx +3 -15
- package/dist/assets/adapters/next/templates/init/pages/dashboard-page.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/forgot-password-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/login-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/reset-password-page-skeleton.tsx +1 -1
- package/dist/assets/adapters/next/templates/init/pages/users/users-page-content.tsx +83 -2
- package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +126 -78
- package/dist/assets/adapters/next/templates/init/utils/editor/tiptap.ts +7 -8
- package/dist/assets/adapters/next/templates/init/utils/form/list-field.ts +17 -0
- package/dist/assets/adapters/next/templates/init/utils/user/user.ts +70 -0
- package/dist/assets/shared-assets/react-admin/custom/content-editor/math-bubble-menu.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/selection-bubble-menu.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +0 -2
- package/dist/assets/shared-assets/react-admin/custom/content-editor/table-button.tsx +17 -65
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-control-popover.tsx +3 -8
- package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-popover.tsx +1 -2
- package/dist/assets/shared-assets/react-admin/custom/date-range-picker.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/dynamic-list-field.tsx +20 -8
- package/dist/assets/shared-assets/react-admin/custom/gallery-field.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/custom/media-gallery-field.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/custom/nested-object-list-field.tsx +211 -0
- package/dist/assets/shared-assets/react-admin/custom/placeholder-card.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/placeholder.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/custom/sortable-gallery-item.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/schema.json +136 -0
- package/dist/assets/shared-assets/react-admin/ui/accordion.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/alert.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/card.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/chart.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/checkbox.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/combobox.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/command.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/context-menu.tsx +6 -6
- package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +6 -6
- package/dist/assets/shared-assets/react-admin/ui/empty.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/field.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/form.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/hover-card.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/item.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/menubar.tsx +7 -7
- package/dist/assets/shared-assets/react-admin/ui/navigation-menu.tsx +4 -4
- package/dist/assets/shared-assets/react-admin/ui/popover.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/resizable.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/select.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +3 -3
- package/dist/assets/shared-assets/react-admin/ui/skeleton.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/tabs.tsx +2 -2
- package/dist/assets/shared-assets/react-admin/ui/textarea.tsx +1 -1
- package/dist/assets/shared-assets/react-admin/ui/tooltip.tsx +1 -1
- package/dist/cli.js +2688 -1644
- package/dist/cli.js.map +1 -1
- package/package.json +10 -2
- package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-filters.tsx +0 -152
|
@@ -4,16 +4,9 @@ import { Button } from '@admin/components/ui/button'
|
|
|
4
4
|
import { Input } from '@admin/components/ui/input'
|
|
5
5
|
import { Label } from '@admin/components/ui/label'
|
|
6
6
|
import { Popover, PopoverContent, PopoverTrigger } from '@admin/components/ui/popover'
|
|
7
|
-
import {
|
|
8
|
-
import { insertContentEditorTable } from '@admin/utils/editor/tiptap'
|
|
7
|
+
import { useTableButton } from '@admin/hooks/content-editor/use-table-button'
|
|
9
8
|
import type { Editor } from '@tiptap/core'
|
|
10
9
|
import { TableIcon } from 'lucide-react'
|
|
11
|
-
import * as React from 'react'
|
|
12
|
-
|
|
13
|
-
const DEFAULT_TABLE_ROWS = 3
|
|
14
|
-
const DEFAULT_TABLE_COLS = 3
|
|
15
|
-
const MIN_TABLE_ROWS = 2
|
|
16
|
-
const MIN_TABLE_COLS = 1
|
|
17
10
|
|
|
18
11
|
interface TableButtonProps {
|
|
19
12
|
disabled?: boolean
|
|
@@ -22,95 +15,54 @@ interface TableButtonProps {
|
|
|
22
15
|
}
|
|
23
16
|
|
|
24
17
|
export function TableButton({ disabled, editor, onInsertTable }: TableButtonProps) {
|
|
25
|
-
const
|
|
26
|
-
const [rows, setRows] = React.useState(String(DEFAULT_TABLE_ROWS))
|
|
27
|
-
const [cols, setCols] = React.useState(String(DEFAULT_TABLE_COLS))
|
|
28
|
-
const rowsId = React.useId()
|
|
29
|
-
const colsId = React.useId()
|
|
30
|
-
const canInsert = Boolean(onInsertTable || editor?.isEditable)
|
|
31
|
-
|
|
32
|
-
const closePopover = React.useCallback(() => {
|
|
33
|
-
setIsOpen(false)
|
|
34
|
-
window.setTimeout(() => setIsOpen(false), 0)
|
|
35
|
-
}, [])
|
|
36
|
-
|
|
37
|
-
const insertTable = React.useCallback(() => {
|
|
38
|
-
if (disabled || !canInsert) return
|
|
39
|
-
|
|
40
|
-
const rowCount = normalizeCount(rows, DEFAULT_TABLE_ROWS, MIN_TABLE_ROWS)
|
|
41
|
-
const colCount = normalizeCount(cols, DEFAULT_TABLE_COLS, MIN_TABLE_COLS)
|
|
42
|
-
|
|
43
|
-
if (onInsertTable) {
|
|
44
|
-
onInsertTable({ cols: colCount, rows: rowCount })
|
|
45
|
-
closePopover()
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (!editor) return
|
|
50
|
-
|
|
51
|
-
insertContentEditorTable(editor, rowCount, colCount)
|
|
52
|
-
closePopover()
|
|
53
|
-
}, [canInsert, closePopover, cols, disabled, editor, onInsertTable, rows])
|
|
54
|
-
|
|
55
|
-
const handleKeyDown = React.useCallback(
|
|
56
|
-
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
57
|
-
if (event.key !== 'Enter') return
|
|
58
|
-
|
|
59
|
-
event.preventDefault()
|
|
60
|
-
event.stopPropagation()
|
|
61
|
-
insertTable()
|
|
62
|
-
},
|
|
63
|
-
[insertTable]
|
|
64
|
-
)
|
|
18
|
+
const tableButton = useTableButton({ disabled, editor, onInsertTable })
|
|
65
19
|
|
|
66
20
|
return (
|
|
67
|
-
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
|
21
|
+
<Popover open={tableButton.isOpen} onOpenChange={tableButton.setIsOpen}>
|
|
68
22
|
<PopoverTrigger asChild>
|
|
69
23
|
<Button
|
|
70
24
|
type="button"
|
|
71
25
|
variant="ghost"
|
|
72
26
|
size="sm"
|
|
73
|
-
disabled={
|
|
27
|
+
disabled={tableButton.isDisabled}
|
|
74
28
|
aria-label="Insert table"
|
|
75
29
|
title="Table"
|
|
76
|
-
onPointerDown={(event) => event.preventDefault()}
|
|
77
|
-
onClick={() => setIsOpen((current) => !current)}
|
|
78
30
|
>
|
|
79
31
|
<TableIcon />
|
|
80
32
|
<span className="tiptap-button-text">Table</span>
|
|
81
33
|
</Button>
|
|
82
34
|
</PopoverTrigger>
|
|
83
35
|
<PopoverContent align="start" className="w-72 p-4">
|
|
84
|
-
<div className="flex flex-col gap-3" onKeyDownCapture={handleKeyDown}>
|
|
36
|
+
<div className="flex flex-col gap-3" onKeyDownCapture={tableButton.handleKeyDown}>
|
|
85
37
|
<div className="grid grid-cols-2 gap-2">
|
|
86
38
|
<div className="flex flex-col gap-1.5">
|
|
87
|
-
<Label htmlFor={rowsId}>Rows</Label>
|
|
39
|
+
<Label htmlFor={tableButton.rowsId}>Rows</Label>
|
|
88
40
|
<Input
|
|
89
|
-
id={rowsId}
|
|
41
|
+
id={tableButton.rowsId}
|
|
90
42
|
inputMode="numeric"
|
|
91
|
-
min={
|
|
92
|
-
onChange={
|
|
43
|
+
min={tableButton.minRows}
|
|
44
|
+
onChange={tableButton.handleRowsChange}
|
|
93
45
|
type="number"
|
|
94
|
-
value={rows}
|
|
46
|
+
value={tableButton.rows}
|
|
95
47
|
/>
|
|
96
48
|
</div>
|
|
97
49
|
<div className="flex flex-col gap-1.5">
|
|
98
|
-
<Label htmlFor={colsId}>Cols</Label>
|
|
50
|
+
<Label htmlFor={tableButton.colsId}>Cols</Label>
|
|
99
51
|
<Input
|
|
100
|
-
id={colsId}
|
|
52
|
+
id={tableButton.colsId}
|
|
101
53
|
inputMode="numeric"
|
|
102
|
-
min={
|
|
103
|
-
onChange={
|
|
54
|
+
min={tableButton.minCols}
|
|
55
|
+
onChange={tableButton.handleColsChange}
|
|
104
56
|
type="number"
|
|
105
|
-
value={cols}
|
|
57
|
+
value={tableButton.cols}
|
|
106
58
|
/>
|
|
107
59
|
</div>
|
|
108
60
|
</div>
|
|
109
61
|
<Button
|
|
110
62
|
type="button"
|
|
111
63
|
variant="default"
|
|
112
|
-
disabled={
|
|
113
|
-
onClick={insertTable}
|
|
64
|
+
disabled={tableButton.isDisabled}
|
|
65
|
+
onClick={tableButton.insertTable}
|
|
114
66
|
>
|
|
115
67
|
Insert
|
|
116
68
|
</Button>
|
|
@@ -9,6 +9,7 @@ import { LinkMain, type LinkMainProps } from './link-main'
|
|
|
9
9
|
|
|
10
10
|
export interface LinkControlPopoverProps extends Omit<LinkButtonProps, 'type'>, LinkMainProps {
|
|
11
11
|
canSet?: boolean
|
|
12
|
+
isActive?: boolean
|
|
12
13
|
label?: string
|
|
13
14
|
onOpenChange?: (isOpen: boolean) => void
|
|
14
15
|
open?: boolean
|
|
@@ -17,7 +18,7 @@ export interface LinkControlPopoverProps extends Omit<LinkButtonProps, 'type'>,
|
|
|
17
18
|
export function LinkControlPopover({
|
|
18
19
|
canSet = true,
|
|
19
20
|
children,
|
|
20
|
-
isActive,
|
|
21
|
+
isActive = false,
|
|
21
22
|
label = 'Link',
|
|
22
23
|
onClick,
|
|
23
24
|
onOpenChange,
|
|
@@ -75,13 +76,7 @@ export function LinkControlPopover({
|
|
|
75
76
|
</PopoverTrigger>
|
|
76
77
|
|
|
77
78
|
<PopoverContent className="w-auto max-w-[calc(100vw-2rem)]">
|
|
78
|
-
<LinkMain
|
|
79
|
-
url={url}
|
|
80
|
-
setUrl={setUrl}
|
|
81
|
-
setLink={handleSetLink}
|
|
82
|
-
removeLink={removeLink}
|
|
83
|
-
isActive={isActive}
|
|
84
|
-
/>
|
|
79
|
+
<LinkMain url={url} setUrl={setUrl} setLink={handleSetLink} removeLink={removeLink} />
|
|
85
80
|
</PopoverContent>
|
|
86
81
|
</Popover>
|
|
87
82
|
)
|
|
@@ -57,7 +57,7 @@ export function LinkPopover({
|
|
|
57
57
|
}: LinkPopoverProps) {
|
|
58
58
|
const { editor } = useTiptapEditor(providedEditor)
|
|
59
59
|
|
|
60
|
-
const { isVisible, canSet, isActive, url, setUrl, setLink, removeLink,
|
|
60
|
+
const { isVisible, canSet, isActive, url, setUrl, setLink, removeLink, label, Icon } =
|
|
61
61
|
useLinkPopover({
|
|
62
62
|
editor,
|
|
63
63
|
hideWhenUnavailable: shouldHideUnavailableControl(availability),
|
|
@@ -105,7 +105,6 @@ export function LinkPopover({
|
|
|
105
105
|
onClick={onClick}
|
|
106
106
|
onOpenChange={handleOnOpenChange}
|
|
107
107
|
open={isOpen}
|
|
108
|
-
openLink={openLink}
|
|
109
108
|
removeLink={removeLink}
|
|
110
109
|
setLink={setLink}
|
|
111
110
|
setUrl={setUrl}
|
|
@@ -139,7 +139,7 @@ export function DateRangePicker({
|
|
|
139
139
|
<ChevronDown />
|
|
140
140
|
</Button>
|
|
141
141
|
</PopoverTrigger>
|
|
142
|
-
<PopoverContent className="w-[282px] gap-0 overflow-hidden rounded-
|
|
142
|
+
<PopoverContent className="w-[282px] gap-0 overflow-hidden rounded-lg p-0" align="start">
|
|
143
143
|
<Calendar
|
|
144
144
|
mode="range"
|
|
145
145
|
month={calendarMonth}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { Button } from '@admin/components/ui/button'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
FormControl,
|
|
6
|
+
FormDescription,
|
|
7
|
+
FormField,
|
|
8
|
+
FormItem,
|
|
9
|
+
FormLabel,
|
|
10
|
+
FormMessage
|
|
11
|
+
} from '@admin/components/ui/form'
|
|
5
12
|
import { Input } from '@admin/components/ui/input'
|
|
6
|
-
import { Label } from '@admin/components/ui/label'
|
|
7
13
|
import { cn } from '@admin/utils/shared/cn'
|
|
8
14
|
import { Plus, Trash2 } from 'lucide-react'
|
|
9
15
|
import { useFieldArray, useFormContext } from 'react-hook-form'
|
|
@@ -46,12 +52,18 @@ export function DynamicListField({
|
|
|
46
52
|
return (
|
|
47
53
|
<div className={cn('space-y-4', className)}>
|
|
48
54
|
<div className="flex items-start justify-between gap-3">
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
<FormField
|
|
56
|
+
control={control}
|
|
57
|
+
name={name}
|
|
58
|
+
render={() => (
|
|
59
|
+
<FormItem className="flex flex-col gap-1">
|
|
60
|
+
<FormLabel className={cn({ 'sr-only': hideLabel })}>{label}</FormLabel>
|
|
61
|
+
{description ? (
|
|
62
|
+
<FormDescription className="text-muted-foreground">{description}</FormDescription>
|
|
63
|
+
) : null}
|
|
64
|
+
</FormItem>
|
|
65
|
+
)}
|
|
66
|
+
/>
|
|
55
67
|
<Button
|
|
56
68
|
type="button"
|
|
57
69
|
variant="outline"
|
|
@@ -97,7 +97,7 @@ export function GalleryField({
|
|
|
97
97
|
<button
|
|
98
98
|
type="button"
|
|
99
99
|
onClick={() => setOpen(true)}
|
|
100
|
-
className="flex aspect-square items-center justify-center rounded-
|
|
100
|
+
className="flex aspect-square items-center justify-center rounded-lg border border-dashed transition-colors hover:border-primary/50 hover:bg-muted/50"
|
|
101
101
|
>
|
|
102
102
|
<Plus className="size-6 text-foreground" />
|
|
103
103
|
</button>
|
|
@@ -111,7 +111,7 @@ export function GalleryField({
|
|
|
111
111
|
type="button"
|
|
112
112
|
onClick={() => !disabled && setOpen(true)}
|
|
113
113
|
disabled={disabled}
|
|
114
|
-
className="flex w-full flex-col items-center justify-center gap-2 rounded-
|
|
114
|
+
className="flex w-full flex-col items-center justify-center gap-2 rounded-lg border border-dashed p-8 transition-colors hover:border-primary/50 hover:bg-muted/50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
115
115
|
>
|
|
116
116
|
<ImageIcon className="size-8 text-foreground" />
|
|
117
117
|
<span className="text-sm text-muted-foreground">Click to select media</span>
|
|
@@ -73,7 +73,7 @@ export function MediaGalleryField({
|
|
|
73
73
|
<div className={cn('group relative', className)}>
|
|
74
74
|
<Card className="p-0">
|
|
75
75
|
<CardContent className="p-0! relative">
|
|
76
|
-
<div className="relative aspect-video rounded-
|
|
76
|
+
<div className="relative aspect-video rounded-lg bg-background overflow-hidden">
|
|
77
77
|
<MediaPreview media={selectedMedia} showMetadata={Boolean(media)} />
|
|
78
78
|
</div>
|
|
79
79
|
|
|
@@ -81,7 +81,7 @@ export function MediaGalleryField({
|
|
|
81
81
|
<>
|
|
82
82
|
<div
|
|
83
83
|
aria-hidden="true"
|
|
84
|
-
className="pointer-events-none absolute inset-0 bg-black/10 opacity-0 backdrop-blur-xs transition-opacity duration-300 ease-out group-hover:opacity-100 rounded-
|
|
84
|
+
className="pointer-events-none absolute inset-0 bg-black/10 opacity-0 backdrop-blur-xs transition-opacity duration-300 ease-out group-hover:opacity-100 rounded-lg"
|
|
85
85
|
/>
|
|
86
86
|
<div className="absolute inset-0 flex items-center justify-center gap-1 opacity-0 transition-opacity duration-200 ease-out group-hover:opacity-100">
|
|
87
87
|
<Button
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Accordion,
|
|
5
|
+
AccordionContent,
|
|
6
|
+
AccordionItem,
|
|
7
|
+
AccordionTrigger
|
|
8
|
+
} from '@admin/components/ui/accordion'
|
|
9
|
+
import { Button } from '@admin/components/ui/button'
|
|
10
|
+
import { Card, CardContent, CardHeader } from '@admin/components/ui/card'
|
|
11
|
+
import { FormDescription, FormField, FormItem, FormLabel } from '@admin/components/ui/form'
|
|
12
|
+
import { cloneDefaultValue, createListItemKey } from '@admin/utils/form/list-field'
|
|
13
|
+
import { cn } from '@admin/utils/shared/cn'
|
|
14
|
+
import { Plus, X } from 'lucide-react'
|
|
15
|
+
import * as React from 'react'
|
|
16
|
+
import { useFormContext, useWatch } from 'react-hook-form'
|
|
17
|
+
|
|
18
|
+
interface NestedObjectListFieldProps {
|
|
19
|
+
name: string
|
|
20
|
+
label: string
|
|
21
|
+
singularLabel: string
|
|
22
|
+
defaultValue: Record<string, unknown>
|
|
23
|
+
children: (index: number) => React.ReactNode
|
|
24
|
+
className?: string
|
|
25
|
+
description?: string
|
|
26
|
+
disabled?: boolean
|
|
27
|
+
hideLabel?: boolean
|
|
28
|
+
maxItems?: number
|
|
29
|
+
renderTitle?: (index: number) => React.ReactNode
|
|
30
|
+
validatePaths?: string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function NestedObjectListField({
|
|
34
|
+
name,
|
|
35
|
+
label,
|
|
36
|
+
singularLabel,
|
|
37
|
+
defaultValue,
|
|
38
|
+
children,
|
|
39
|
+
className,
|
|
40
|
+
description,
|
|
41
|
+
disabled,
|
|
42
|
+
hideLabel,
|
|
43
|
+
maxItems,
|
|
44
|
+
renderTitle,
|
|
45
|
+
validatePaths
|
|
46
|
+
}: NestedObjectListFieldProps) {
|
|
47
|
+
const { control, setValue, trigger } = useFormContext()
|
|
48
|
+
const watchedItems = useWatch({
|
|
49
|
+
control,
|
|
50
|
+
name
|
|
51
|
+
})
|
|
52
|
+
const items = Array.isArray(watchedItems) ? watchedItems : []
|
|
53
|
+
const [expanded, setExpanded] = React.useState('')
|
|
54
|
+
const [itemKeys, setItemKeys] = React.useState<string[]>([])
|
|
55
|
+
const canAddMore = !maxItems || items.length < maxItems
|
|
56
|
+
|
|
57
|
+
React.useEffect(() => {
|
|
58
|
+
setItemKeys((currentKeys) => {
|
|
59
|
+
if (currentKeys.length === items.length) return currentKeys
|
|
60
|
+
if (currentKeys.length > items.length) {
|
|
61
|
+
return currentKeys.slice(0, items.length)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const missingKeyCount = items.length - currentKeys.length
|
|
65
|
+
const nextKeys = Array.from({ length: missingKeyCount }, createListItemKey)
|
|
66
|
+
return [...currentKeys, ...nextKeys]
|
|
67
|
+
})
|
|
68
|
+
}, [items.length])
|
|
69
|
+
|
|
70
|
+
async function handleAdd() {
|
|
71
|
+
if (!canAddMore) return
|
|
72
|
+
if (expanded) {
|
|
73
|
+
const isExpandedItemValid = await validateExpandedItem()
|
|
74
|
+
if (!isExpandedItemValid) return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const nextValue = cloneDefaultValue(defaultValue) as Record<string, unknown>
|
|
78
|
+
const nextItems = [...items, nextValue]
|
|
79
|
+
setValue(name, nextItems, {
|
|
80
|
+
shouldDirty: true,
|
|
81
|
+
shouldValidate: false
|
|
82
|
+
})
|
|
83
|
+
setItemKeys((currentKeys) => [...currentKeys, createListItemKey()])
|
|
84
|
+
setExpanded(`item-${nextItems.length}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const handleRemove = (index: number) => {
|
|
88
|
+
if (disabled) return
|
|
89
|
+
|
|
90
|
+
const nextItems = items.filter((_, itemIndex) => itemIndex !== index)
|
|
91
|
+
setValue(name, nextItems, {
|
|
92
|
+
shouldDirty: true,
|
|
93
|
+
shouldValidate: false
|
|
94
|
+
})
|
|
95
|
+
const keepOtherKeys = (_: string, itemIndex: number) => itemIndex !== index
|
|
96
|
+
setItemKeys((currentKeys) => currentKeys.filter(keepOtherKeys))
|
|
97
|
+
setExpanded('')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function validateExpandedItem() {
|
|
101
|
+
if (!expanded) return true
|
|
102
|
+
|
|
103
|
+
const expandedIndex = Number(expanded.replace('item-', '')) - 1
|
|
104
|
+
if (expandedIndex < 0) return true
|
|
105
|
+
|
|
106
|
+
const itemPath = `${name}.${expandedIndex}`
|
|
107
|
+
const paths =
|
|
108
|
+
validatePaths && validatePaths.length > 0
|
|
109
|
+
? validatePaths.map((path) => `${itemPath}.${path}`)
|
|
110
|
+
: [itemPath]
|
|
111
|
+
|
|
112
|
+
return trigger(paths)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function handleExpandedChange(nextExpanded: string) {
|
|
116
|
+
if (expanded && nextExpanded && nextExpanded !== expanded) {
|
|
117
|
+
const isExpandedItemValid = await validateExpandedItem()
|
|
118
|
+
if (!isExpandedItemValid) return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setExpanded(nextExpanded)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const getTitle = (index: number) => {
|
|
125
|
+
if (renderTitle) return renderTitle(index)
|
|
126
|
+
return `${singularLabel} ${index + 1}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<FormItem className={cn('space-y-4', className)}>
|
|
131
|
+
{items.length === 0 ? (
|
|
132
|
+
<Card>
|
|
133
|
+
<CardContent className="flex flex-col items-center justify-center">
|
|
134
|
+
<p className="text-muted-foreground">No {label} added yet.</p>
|
|
135
|
+
<Button
|
|
136
|
+
type="button"
|
|
137
|
+
variant="outline"
|
|
138
|
+
onClick={handleAdd}
|
|
139
|
+
disabled={disabled || !canAddMore}
|
|
140
|
+
>
|
|
141
|
+
<Plus className="size-3" />
|
|
142
|
+
Add {singularLabel}
|
|
143
|
+
</Button>
|
|
144
|
+
</CardContent>
|
|
145
|
+
</Card>
|
|
146
|
+
) : (
|
|
147
|
+
<Card className="space-y-4 border-t border-border pt-4">
|
|
148
|
+
<CardHeader className="flex items-center justify-between pl-5">
|
|
149
|
+
<FormField
|
|
150
|
+
control={control}
|
|
151
|
+
name={name}
|
|
152
|
+
render={() => (
|
|
153
|
+
<FormItem className="flex flex-col gap-1">
|
|
154
|
+
<FormLabel className={cn({ 'sr-only': hideLabel })}>{label}</FormLabel>
|
|
155
|
+
{description ? <FormDescription>{description}</FormDescription> : null}
|
|
156
|
+
</FormItem>
|
|
157
|
+
)}
|
|
158
|
+
/>
|
|
159
|
+
<Button
|
|
160
|
+
type="button"
|
|
161
|
+
variant="outline"
|
|
162
|
+
onClick={handleAdd}
|
|
163
|
+
disabled={disabled || !canAddMore}
|
|
164
|
+
>
|
|
165
|
+
<Plus className="size-3" />
|
|
166
|
+
Add {singularLabel}
|
|
167
|
+
</Button>
|
|
168
|
+
</CardHeader>
|
|
169
|
+
|
|
170
|
+
<CardContent className="p-0!">
|
|
171
|
+
<Accordion
|
|
172
|
+
type="single"
|
|
173
|
+
collapsible
|
|
174
|
+
value={expanded}
|
|
175
|
+
onValueChange={handleExpandedChange}
|
|
176
|
+
className="border-none"
|
|
177
|
+
>
|
|
178
|
+
{items.map((_, index) => (
|
|
179
|
+
<AccordionItem
|
|
180
|
+
key={itemKeys[index] ?? `${name}-${index}`}
|
|
181
|
+
value={`item-${index + 1}`}
|
|
182
|
+
className="bg-transparent!"
|
|
183
|
+
>
|
|
184
|
+
<div className="relative py-1">
|
|
185
|
+
<AccordionTrigger className="flex min-h-12 w-full items-center gap-2 py-2">
|
|
186
|
+
<span className="w-full pr-16">{getTitle(index)}</span>
|
|
187
|
+
</AccordionTrigger>
|
|
188
|
+
<Button
|
|
189
|
+
type="button"
|
|
190
|
+
variant="ghost"
|
|
191
|
+
size="icon-sm"
|
|
192
|
+
className="absolute right-10 top-1/2 shrink-0 -translate-y-1/2"
|
|
193
|
+
aria-label={`Remove ${singularLabel}`}
|
|
194
|
+
onClick={() => handleRemove(index)}
|
|
195
|
+
disabled={disabled}
|
|
196
|
+
>
|
|
197
|
+
<X />
|
|
198
|
+
</Button>
|
|
199
|
+
</div>
|
|
200
|
+
<AccordionContent className="space-y-5 border rounded-lg border-border p-5 bg-background mb-4">
|
|
201
|
+
{children(index)}
|
|
202
|
+
</AccordionContent>
|
|
203
|
+
</AccordionItem>
|
|
204
|
+
))}
|
|
205
|
+
</Accordion>
|
|
206
|
+
</CardContent>
|
|
207
|
+
</Card>
|
|
208
|
+
)}
|
|
209
|
+
</FormItem>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
@@ -19,7 +19,7 @@ export function PlaceholderCard({
|
|
|
19
19
|
return (
|
|
20
20
|
<div
|
|
21
21
|
className={cn(
|
|
22
|
-
'flex h-75 border border-border border-dashed bg-background flex-col items-center justify-center gap-5 rounded-
|
|
22
|
+
'flex h-75 border border-border border-dashed bg-background flex-col items-center justify-center gap-5 rounded-lg',
|
|
23
23
|
className
|
|
24
24
|
)}
|
|
25
25
|
>
|
|
@@ -12,7 +12,7 @@ export function Placeholder({ label, description, action, className, ...props }:
|
|
|
12
12
|
return (
|
|
13
13
|
<div
|
|
14
14
|
className={cn(
|
|
15
|
-
'border flex items-center flex-col gap-2 border-dashed border-border rounded-
|
|
15
|
+
'border flex items-center flex-col gap-2 border-dashed border-border rounded-lg p-8 bg-background',
|
|
16
16
|
className
|
|
17
17
|
)}
|
|
18
18
|
{...props}
|
|
@@ -31,7 +31,7 @@ export function SortableGalleryItem({
|
|
|
31
31
|
<div
|
|
32
32
|
ref={setNodeRef}
|
|
33
33
|
style={style}
|
|
34
|
-
className="group relative aspect-square overflow-hidden rounded-
|
|
34
|
+
className="group relative aspect-square overflow-hidden rounded-lg border bg-muted"
|
|
35
35
|
>
|
|
36
36
|
{!disabled && (
|
|
37
37
|
<div
|
|
@@ -1441,6 +1441,111 @@
|
|
|
1441
1441
|
"$ref": "#/$defs/fieldWithCreatableSelect"
|
|
1442
1442
|
}
|
|
1443
1443
|
},
|
|
1444
|
+
"fieldArrayWithTitle": {
|
|
1445
|
+
"type": "array",
|
|
1446
|
+
"contains": {
|
|
1447
|
+
"$ref": "#/$defs/fieldWithTitle"
|
|
1448
|
+
}
|
|
1449
|
+
},
|
|
1450
|
+
"fieldWithTitle": {
|
|
1451
|
+
"type": "object",
|
|
1452
|
+
"anyOf": [
|
|
1453
|
+
{
|
|
1454
|
+
"properties": {
|
|
1455
|
+
"name": {
|
|
1456
|
+
"const": "title"
|
|
1457
|
+
},
|
|
1458
|
+
"type": {
|
|
1459
|
+
"enum": ["string", "varchar", "text"]
|
|
1460
|
+
}
|
|
1461
|
+
},
|
|
1462
|
+
"required": ["name", "type"]
|
|
1463
|
+
},
|
|
1464
|
+
{
|
|
1465
|
+
"properties": {
|
|
1466
|
+
"type": {
|
|
1467
|
+
"enum": ["group", "section"]
|
|
1468
|
+
},
|
|
1469
|
+
"fields": {
|
|
1470
|
+
"$ref": "#/$defs/fieldArrayWithTitle"
|
|
1471
|
+
}
|
|
1472
|
+
},
|
|
1473
|
+
"required": ["type", "fields"]
|
|
1474
|
+
},
|
|
1475
|
+
{
|
|
1476
|
+
"properties": {
|
|
1477
|
+
"tabs": {
|
|
1478
|
+
"type": "array",
|
|
1479
|
+
"contains": {
|
|
1480
|
+
"$ref": "#/$defs/tabWithTitle"
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
},
|
|
1484
|
+
"required": ["tabs"]
|
|
1485
|
+
}
|
|
1486
|
+
]
|
|
1487
|
+
},
|
|
1488
|
+
"tabWithTitle": {
|
|
1489
|
+
"type": "object",
|
|
1490
|
+
"anyOf": [
|
|
1491
|
+
{
|
|
1492
|
+
"properties": {
|
|
1493
|
+
"fields": {
|
|
1494
|
+
"$ref": "#/$defs/fieldArrayWithTitle"
|
|
1495
|
+
}
|
|
1496
|
+
},
|
|
1497
|
+
"required": ["fields"]
|
|
1498
|
+
},
|
|
1499
|
+
{
|
|
1500
|
+
"properties": {
|
|
1501
|
+
"slot": {
|
|
1502
|
+
"$ref": "#/$defs/slotLayoutWithTitle"
|
|
1503
|
+
}
|
|
1504
|
+
},
|
|
1505
|
+
"required": ["slot"]
|
|
1506
|
+
}
|
|
1507
|
+
]
|
|
1508
|
+
},
|
|
1509
|
+
"slotLayoutWithTitle": {
|
|
1510
|
+
"type": "object",
|
|
1511
|
+
"anyOf": [
|
|
1512
|
+
{
|
|
1513
|
+
"properties": {
|
|
1514
|
+
"main": {
|
|
1515
|
+
"properties": {
|
|
1516
|
+
"fields": {
|
|
1517
|
+
"$ref": "#/$defs/fieldArrayWithTitle"
|
|
1518
|
+
}
|
|
1519
|
+
},
|
|
1520
|
+
"required": ["fields"]
|
|
1521
|
+
}
|
|
1522
|
+
},
|
|
1523
|
+
"required": ["main"]
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
"properties": {
|
|
1527
|
+
"sidebar": {
|
|
1528
|
+
"properties": {
|
|
1529
|
+
"fields": {
|
|
1530
|
+
"$ref": "#/$defs/fieldArrayWithTitle"
|
|
1531
|
+
}
|
|
1532
|
+
},
|
|
1533
|
+
"required": ["fields"]
|
|
1534
|
+
}
|
|
1535
|
+
},
|
|
1536
|
+
"required": ["sidebar"]
|
|
1537
|
+
}
|
|
1538
|
+
]
|
|
1539
|
+
},
|
|
1540
|
+
"stepWithTitle": {
|
|
1541
|
+
"type": "object",
|
|
1542
|
+
"properties": {
|
|
1543
|
+
"fields": {
|
|
1544
|
+
"$ref": "#/$defs/fieldArrayWithTitle"
|
|
1545
|
+
}
|
|
1546
|
+
},
|
|
1547
|
+
"required": ["fields"]
|
|
1548
|
+
},
|
|
1444
1549
|
"fieldWithCreatableSelect": {
|
|
1445
1550
|
"type": "object",
|
|
1446
1551
|
"anyOf": [
|
|
@@ -1611,6 +1716,37 @@
|
|
|
1611
1716
|
}
|
|
1612
1717
|
],
|
|
1613
1718
|
"allOf": [
|
|
1719
|
+
{
|
|
1720
|
+
"anyOf": [
|
|
1721
|
+
{
|
|
1722
|
+
"properties": {
|
|
1723
|
+
"fields": {
|
|
1724
|
+
"$ref": "#/$defs/fieldArrayWithTitle"
|
|
1725
|
+
}
|
|
1726
|
+
},
|
|
1727
|
+
"required": ["fields"]
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
"properties": {
|
|
1731
|
+
"slot": {
|
|
1732
|
+
"$ref": "#/$defs/slotLayoutWithTitle"
|
|
1733
|
+
}
|
|
1734
|
+
},
|
|
1735
|
+
"required": ["slot"]
|
|
1736
|
+
},
|
|
1737
|
+
{
|
|
1738
|
+
"properties": {
|
|
1739
|
+
"steps": {
|
|
1740
|
+
"type": "array",
|
|
1741
|
+
"contains": {
|
|
1742
|
+
"$ref": "#/$defs/stepWithTitle"
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
},
|
|
1746
|
+
"required": ["steps"]
|
|
1747
|
+
}
|
|
1748
|
+
]
|
|
1749
|
+
},
|
|
1614
1750
|
{
|
|
1615
1751
|
"if": {
|
|
1616
1752
|
"properties": {
|
|
@@ -10,7 +10,7 @@ function Accordion({ className, ...props }: React.ComponentProps<typeof Accordio
|
|
|
10
10
|
<AccordionPrimitive.Root
|
|
11
11
|
data-slot="accordion"
|
|
12
12
|
className={cn(
|
|
13
|
-
'flex w-full flex-col rounded-
|
|
13
|
+
'flex w-full flex-col rounded-lg overflow-hidden border border-border bg-card shadow-2xl/1',
|
|
14
14
|
className
|
|
15
15
|
)}
|
|
16
16
|
{...props}
|