@cero-base/panel 0.0.1
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/README.md +42 -0
- package/package.json +41 -0
- package/src/components/data-table.jsx +245 -0
- package/src/components/document-form.jsx +211 -0
- package/src/components/panel.jsx +1131 -0
- package/src/components/sidebar.jsx +151 -0
- package/src/components/ui/avatar.jsx +38 -0
- package/src/components/ui/badge.jsx +26 -0
- package/src/components/ui/button.jsx +51 -0
- package/src/components/ui/checkbox.jsx +43 -0
- package/src/components/ui/dialog.jsx +126 -0
- package/src/components/ui/dropdown-menu.jsx +213 -0
- package/src/components/ui/input.jsx +19 -0
- package/src/components/ui/resizable.jsx +34 -0
- package/src/components/ui/scroll-area.jsx +32 -0
- package/src/components/ui/select.jsx +169 -0
- package/src/components/ui/table.jsx +88 -0
- package/src/components/ui/tabs.jsx +43 -0
- package/src/components/ui/tooltip.jsx +33 -0
- package/src/global.css +86 -0
- package/src/hooks/index.js +3 -0
- package/src/hooks/use-collection.js +27 -0
- package/src/hooks/use-rooms.js +80 -0
- package/src/hooks/use-schema.js +71 -0
- package/src/hooks/use-system.js +23 -0
- package/src/hooks/util.js +10 -0
- package/src/index.js +5 -0
- package/src/lib/field-types.js +148 -0
- package/src/lib/utils.js +19 -0
- package/src/open-panel.js +30 -0
- package/src/panel-window.html +24 -0
- package/src/panel-window.jsx +195 -0
- package/src/theme.css +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @cero-base/panel
|
|
2
|
+
|
|
3
|
+
Debug panel component for cero-base. Displays schema, collections, rooms, and system data in a collapsible panel overlay.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @cero-base/panel
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
import { CeroPanel } from '@cero-base/panel'
|
|
15
|
+
|
|
16
|
+
function App() {
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<MyApp />
|
|
20
|
+
<CeroPanel db={db} />
|
|
21
|
+
</>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Exports
|
|
27
|
+
|
|
28
|
+
| Export | Description |
|
|
29
|
+
| --------------------- | --------------------------------- |
|
|
30
|
+
| `CeroPanel` | Debug panel component |
|
|
31
|
+
| `useSchema` | Hook for schema introspection |
|
|
32
|
+
| `useSchemaByScope` | Hook for schema filtered by scope |
|
|
33
|
+
| `useCollectionSchema` | Hook for single collection schema |
|
|
34
|
+
| `useCollection` | Hook for collection data |
|
|
35
|
+
| `useRooms` | Hook for room list |
|
|
36
|
+
| `useRoomSelection` | Hook for room selection state |
|
|
37
|
+
| `useIdentityData` | Hook for identity system data |
|
|
38
|
+
| `useRoomData` | Hook for room system data |
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
Apache-2.0
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cero-base/panel",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Admin panel UI for cero-base",
|
|
5
|
+
"files": [
|
|
6
|
+
"src",
|
|
7
|
+
"README.md"
|
|
8
|
+
],
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "src/index.js",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./src/index.js",
|
|
16
|
+
"./global.css": "./src/global.css"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@base-ui/react": "^1.0.0",
|
|
20
|
+
"@cero-base/core": "^0.0.1",
|
|
21
|
+
"@cero-base/react": "^0.0.1",
|
|
22
|
+
"@tanstack/react-table": "^8.0.0",
|
|
23
|
+
"b4a": "^1.6.4",
|
|
24
|
+
"class-variance-authority": "^0.7.0",
|
|
25
|
+
"clsx": "^2.1.0",
|
|
26
|
+
"lucide-react": "^0.400.0",
|
|
27
|
+
"react-resizable-panels": "^2.0.0",
|
|
28
|
+
"streamx": "^2.23.0",
|
|
29
|
+
"tailwind-merge": "^2.0.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": ">=18",
|
|
33
|
+
"react-dom": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"license": "Apache-2.0",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/lekinox/cero-base.git",
|
|
39
|
+
"directory": "packages/panel"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { useState, useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
flexRender,
|
|
4
|
+
getCoreRowModel,
|
|
5
|
+
getSortedRowModel,
|
|
6
|
+
getPaginationRowModel,
|
|
7
|
+
useReactTable
|
|
8
|
+
} from '@tanstack/react-table'
|
|
9
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table'
|
|
10
|
+
import { Checkbox } from './ui/checkbox'
|
|
11
|
+
import { Button } from './ui/button'
|
|
12
|
+
import {
|
|
13
|
+
DropdownMenu,
|
|
14
|
+
DropdownMenuContent,
|
|
15
|
+
DropdownMenuItem,
|
|
16
|
+
DropdownMenuTrigger
|
|
17
|
+
} from './ui/dropdown-menu'
|
|
18
|
+
import { formatValue, truncate } from '../lib/field-types'
|
|
19
|
+
import { cn } from '../lib/utils'
|
|
20
|
+
import {
|
|
21
|
+
ArrowUpDown,
|
|
22
|
+
ArrowUp,
|
|
23
|
+
ArrowDown,
|
|
24
|
+
MoreHorizontal,
|
|
25
|
+
Pencil,
|
|
26
|
+
Trash2,
|
|
27
|
+
ChevronLeft,
|
|
28
|
+
ChevronRight
|
|
29
|
+
} from 'lucide-react'
|
|
30
|
+
|
|
31
|
+
function SortButton({ column, children }) {
|
|
32
|
+
const sorted = column.getIsSorted()
|
|
33
|
+
return (
|
|
34
|
+
<button
|
|
35
|
+
onClick={() => column.toggleSorting(sorted === 'asc')}
|
|
36
|
+
className='flex items-center gap-1 hover:text-foreground'
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
{sorted === 'asc' ? (
|
|
40
|
+
<ArrowUp className='size-3' />
|
|
41
|
+
) : sorted === 'desc' ? (
|
|
42
|
+
<ArrowDown className='size-3' />
|
|
43
|
+
) : (
|
|
44
|
+
<ArrowUpDown className='size-3 opacity-50' />
|
|
45
|
+
)}
|
|
46
|
+
</button>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ActionCell({ row, onEdit, onDelete }) {
|
|
51
|
+
return (
|
|
52
|
+
<DropdownMenu>
|
|
53
|
+
<DropdownMenuTrigger asChild>
|
|
54
|
+
<Button variant='ghost' size='icon-sm' className='size-6'>
|
|
55
|
+
<MoreHorizontal className='size-3.5' />
|
|
56
|
+
<span className='sr-only'>Actions</span>
|
|
57
|
+
</Button>
|
|
58
|
+
</DropdownMenuTrigger>
|
|
59
|
+
<DropdownMenuContent align='end'>
|
|
60
|
+
<DropdownMenuItem onClick={() => onEdit(row.original)}>
|
|
61
|
+
<Pencil />
|
|
62
|
+
Edit
|
|
63
|
+
</DropdownMenuItem>
|
|
64
|
+
<DropdownMenuItem variant='destructive' onClick={() => onDelete(row.original)}>
|
|
65
|
+
<Trash2 />
|
|
66
|
+
Delete
|
|
67
|
+
</DropdownMenuItem>
|
|
68
|
+
</DropdownMenuContent>
|
|
69
|
+
</DropdownMenu>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function Pagination({ table }) {
|
|
74
|
+
return (
|
|
75
|
+
<div className='flex items-center justify-between px-2 py-2 border-t border-border/50'>
|
|
76
|
+
<div className='text-xs text-muted-foreground'>
|
|
77
|
+
{table.getFilteredSelectedRowModel().rows.length} of{' '}
|
|
78
|
+
{table.getFilteredRowModel().rows.length} selected
|
|
79
|
+
</div>
|
|
80
|
+
<div className='flex items-center gap-1'>
|
|
81
|
+
<span className='text-xs text-muted-foreground mr-2'>
|
|
82
|
+
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
|
83
|
+
</span>
|
|
84
|
+
<Button
|
|
85
|
+
variant='outline'
|
|
86
|
+
size='icon-sm'
|
|
87
|
+
onClick={() => table.previousPage()}
|
|
88
|
+
disabled={!table.getCanPreviousPage()}
|
|
89
|
+
>
|
|
90
|
+
<ChevronLeft className='size-3.5' />
|
|
91
|
+
</Button>
|
|
92
|
+
<Button
|
|
93
|
+
variant='outline'
|
|
94
|
+
size='icon-sm'
|
|
95
|
+
onClick={() => table.nextPage()}
|
|
96
|
+
disabled={!table.getCanNextPage()}
|
|
97
|
+
>
|
|
98
|
+
<ChevronRight className='size-3.5' />
|
|
99
|
+
</Button>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function DataTable({ data, schema, loading, onEdit, onDelete, onSelectionChange }) {
|
|
106
|
+
const [sorting, setSorting] = useState([])
|
|
107
|
+
const [rowSelection, setRowSelection] = useState({})
|
|
108
|
+
|
|
109
|
+
// Build columns from schema
|
|
110
|
+
const columns = useMemo(() => {
|
|
111
|
+
if (!schema?.fields) return []
|
|
112
|
+
|
|
113
|
+
const cols = [
|
|
114
|
+
// Selection column
|
|
115
|
+
{
|
|
116
|
+
id: 'select',
|
|
117
|
+
header: ({ table }) => (
|
|
118
|
+
<Checkbox
|
|
119
|
+
checked={
|
|
120
|
+
table.getIsAllPageRowsSelected() ||
|
|
121
|
+
(table.getIsSomePageRowsSelected() && 'indeterminate')
|
|
122
|
+
}
|
|
123
|
+
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
124
|
+
aria-label='Select all'
|
|
125
|
+
/>
|
|
126
|
+
),
|
|
127
|
+
cell: ({ row }) => (
|
|
128
|
+
<Checkbox
|
|
129
|
+
checked={row.getIsSelected()}
|
|
130
|
+
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
131
|
+
aria-label='Select row'
|
|
132
|
+
/>
|
|
133
|
+
),
|
|
134
|
+
enableSorting: false,
|
|
135
|
+
size: 40
|
|
136
|
+
},
|
|
137
|
+
// Field columns from schema
|
|
138
|
+
...Object.entries(schema.fields).map(([field, type]) => ({
|
|
139
|
+
accessorKey: field,
|
|
140
|
+
header: ({ column }) => <SortButton column={column}>{field}</SortButton>,
|
|
141
|
+
cell: ({ row }) => {
|
|
142
|
+
const value = row.getValue(field)
|
|
143
|
+
const formatted = formatValue(value, type)
|
|
144
|
+
return (
|
|
145
|
+
<span
|
|
146
|
+
className={cn(schema.key.includes(field) && 'font-medium', 'max-w-48 block truncate')}
|
|
147
|
+
title={formatted}
|
|
148
|
+
>
|
|
149
|
+
{truncate(formatted, 40)}
|
|
150
|
+
</span>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
})),
|
|
154
|
+
// Actions column
|
|
155
|
+
{
|
|
156
|
+
id: 'actions',
|
|
157
|
+
cell: ({ row }) => <ActionCell row={row} onEdit={onEdit} onDelete={onDelete} />,
|
|
158
|
+
size: 40
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
return cols
|
|
163
|
+
}, [schema, onEdit, onDelete])
|
|
164
|
+
|
|
165
|
+
const table = useReactTable({
|
|
166
|
+
data: data || [],
|
|
167
|
+
columns,
|
|
168
|
+
state: {
|
|
169
|
+
sorting,
|
|
170
|
+
rowSelection
|
|
171
|
+
},
|
|
172
|
+
onSortingChange: setSorting,
|
|
173
|
+
onRowSelectionChange: (updater) => {
|
|
174
|
+
const newSelection = typeof updater === 'function' ? updater(rowSelection) : updater
|
|
175
|
+
setRowSelection(newSelection)
|
|
176
|
+
if (onSelectionChange) {
|
|
177
|
+
const selectedRows = Object.keys(newSelection)
|
|
178
|
+
.filter((key) => newSelection[key])
|
|
179
|
+
.map((key) => data[parseInt(key)])
|
|
180
|
+
onSelectionChange(selectedRows)
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
getCoreRowModel: getCoreRowModel(),
|
|
184
|
+
getSortedRowModel: getSortedRowModel(),
|
|
185
|
+
getPaginationRowModel: getPaginationRowModel()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
if (loading) {
|
|
189
|
+
return (
|
|
190
|
+
<div className='flex h-40 items-center justify-center text-xs text-muted-foreground'>
|
|
191
|
+
Loading...
|
|
192
|
+
</div>
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!schema) {
|
|
197
|
+
return (
|
|
198
|
+
<div className='flex h-40 items-center justify-center text-xs text-muted-foreground'>
|
|
199
|
+
Select a collection to view data
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (data.length === 0) {
|
|
205
|
+
return (
|
|
206
|
+
<div className='flex h-40 items-center justify-center text-xs text-muted-foreground'>
|
|
207
|
+
No documents found
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div className='flex flex-col h-full'>
|
|
214
|
+
<div className='flex-1 overflow-auto'>
|
|
215
|
+
<Table>
|
|
216
|
+
<TableHeader>
|
|
217
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
218
|
+
<TableRow key={headerGroup.id}>
|
|
219
|
+
{headerGroup.headers.map((header) => (
|
|
220
|
+
<TableHead key={header.id} style={{ width: header.column.getSize() }}>
|
|
221
|
+
{header.isPlaceholder
|
|
222
|
+
? null
|
|
223
|
+
: flexRender(header.column.columnDef.header, header.getContext())}
|
|
224
|
+
</TableHead>
|
|
225
|
+
))}
|
|
226
|
+
</TableRow>
|
|
227
|
+
))}
|
|
228
|
+
</TableHeader>
|
|
229
|
+
<TableBody>
|
|
230
|
+
{table.getRowModel().rows.map((row) => (
|
|
231
|
+
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
|
|
232
|
+
{row.getVisibleCells().map((cell) => (
|
|
233
|
+
<TableCell key={cell.id}>
|
|
234
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
235
|
+
</TableCell>
|
|
236
|
+
))}
|
|
237
|
+
</TableRow>
|
|
238
|
+
))}
|
|
239
|
+
</TableBody>
|
|
240
|
+
</Table>
|
|
241
|
+
</div>
|
|
242
|
+
<Pagination table={table} />
|
|
243
|
+
</div>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
DialogHeader,
|
|
6
|
+
DialogTitle,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogFooter
|
|
9
|
+
} from './ui/dialog'
|
|
10
|
+
import { Button } from './ui/button'
|
|
11
|
+
import { Input } from './ui/input'
|
|
12
|
+
import { Checkbox } from './ui/checkbox'
|
|
13
|
+
import { ScrollArea } from './ui/scroll-area'
|
|
14
|
+
import { getInputType, parseValue, getDefaultValue } from '../lib/field-types'
|
|
15
|
+
import { cn } from '../lib/utils'
|
|
16
|
+
|
|
17
|
+
function FieldInput({ name, type, value, onChange, disabled }) {
|
|
18
|
+
const inputType = getInputType(type)
|
|
19
|
+
const typeName = typeof type === 'string' ? type : type?.type
|
|
20
|
+
|
|
21
|
+
if (inputType === 'checkbox') {
|
|
22
|
+
return (
|
|
23
|
+
<div className='flex items-center gap-2'>
|
|
24
|
+
<Checkbox
|
|
25
|
+
id={name}
|
|
26
|
+
checked={Boolean(value)}
|
|
27
|
+
onCheckedChange={(checked) => onChange(checked)}
|
|
28
|
+
disabled={disabled}
|
|
29
|
+
/>
|
|
30
|
+
<label htmlFor={name} className='text-xs'>
|
|
31
|
+
{value ? 'true' : 'false'}
|
|
32
|
+
</label>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (inputType === 'textarea') {
|
|
38
|
+
const displayValue =
|
|
39
|
+
typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value || '')
|
|
40
|
+
return (
|
|
41
|
+
<textarea
|
|
42
|
+
id={name}
|
|
43
|
+
value={displayValue}
|
|
44
|
+
onChange={(e) => onChange(parseValue(e.target.value, type))}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
className={cn(
|
|
47
|
+
'flex min-h-20 w-full rounded-md border border-input bg-input/20 px-3 py-2 text-xs',
|
|
48
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
49
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
50
|
+
'font-mono'
|
|
51
|
+
)}
|
|
52
|
+
rows={4}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Input
|
|
59
|
+
id={name}
|
|
60
|
+
type={inputType}
|
|
61
|
+
value={value ?? ''}
|
|
62
|
+
onChange={(e) => {
|
|
63
|
+
const val = inputType === 'number' ? parseValue(e.target.value, type) : e.target.value
|
|
64
|
+
onChange(val)
|
|
65
|
+
}}
|
|
66
|
+
disabled={disabled}
|
|
67
|
+
className='text-xs'
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function FormField({ name, type, value, onChange, disabled, isKey }) {
|
|
73
|
+
const typeName = typeof type === 'string' ? type : type?.type
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className='space-y-1.5'>
|
|
77
|
+
<label htmlFor={name} className='flex items-center gap-2 text-xs font-medium'>
|
|
78
|
+
{name}
|
|
79
|
+
{isKey && (
|
|
80
|
+
<span className='text-[10px] text-muted-foreground px-1 py-0.5 bg-muted rounded'>
|
|
81
|
+
key
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
<span className='text-[10px] text-muted-foreground ml-auto'>{typeName || 'string'}</span>
|
|
85
|
+
</label>
|
|
86
|
+
<FieldInput name={name} type={type} value={value} onChange={onChange} disabled={disabled} />
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function DocumentForm({ open, onOpenChange, schema, doc, onSave, onDelete }) {
|
|
92
|
+
const isNew = !doc || Object.keys(doc).length === 0
|
|
93
|
+
const [values, setValues] = useState({})
|
|
94
|
+
const [saving, setSaving] = useState(false)
|
|
95
|
+
const [deleting, setDeleting] = useState(false)
|
|
96
|
+
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
97
|
+
|
|
98
|
+
// Initialize values when doc changes
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!schema?.fields) {
|
|
101
|
+
setValues({})
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (isNew) {
|
|
106
|
+
// Set default values for new doc
|
|
107
|
+
const defaults = Object.entries(schema.fields).reduce((acc, [field, type]) => {
|
|
108
|
+
acc[field] = getDefaultValue(type)
|
|
109
|
+
return acc
|
|
110
|
+
}, {})
|
|
111
|
+
setValues(defaults)
|
|
112
|
+
} else {
|
|
113
|
+
setValues({ ...doc })
|
|
114
|
+
}
|
|
115
|
+
}, [doc, schema, isNew])
|
|
116
|
+
|
|
117
|
+
const onChange = (field, value) => {
|
|
118
|
+
setValues((prev) => ({ ...prev, [field]: value }))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const handleSave = async () => {
|
|
122
|
+
setSaving(true)
|
|
123
|
+
try {
|
|
124
|
+
await onSave(values, isNew)
|
|
125
|
+
onOpenChange(false)
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error('Save failed:', err)
|
|
128
|
+
} finally {
|
|
129
|
+
setSaving(false)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const handleDelete = async () => {
|
|
134
|
+
if (!confirmDelete) {
|
|
135
|
+
setConfirmDelete(true)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
setDeleting(true)
|
|
140
|
+
try {
|
|
141
|
+
await onDelete(doc)
|
|
142
|
+
onOpenChange(false)
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error('Delete failed:', err)
|
|
145
|
+
} finally {
|
|
146
|
+
setDeleting(false)
|
|
147
|
+
setConfirmDelete(false)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const onClose = (open) => {
|
|
152
|
+
if (!open) {
|
|
153
|
+
setConfirmDelete(false)
|
|
154
|
+
}
|
|
155
|
+
onOpenChange(open)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!schema) return null
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<Dialog open={open} onOpenChange={onClose}>
|
|
162
|
+
<DialogContent className='sm:max-w-md'>
|
|
163
|
+
<DialogHeader>
|
|
164
|
+
<DialogTitle>{isNew ? 'New Document' : 'Edit Document'}</DialogTitle>
|
|
165
|
+
<DialogDescription>
|
|
166
|
+
{isNew
|
|
167
|
+
? `Create a new document in ${schema.name}`
|
|
168
|
+
: `Editing document in ${schema.name}`}
|
|
169
|
+
</DialogDescription>
|
|
170
|
+
</DialogHeader>
|
|
171
|
+
<ScrollArea className='max-h-96'>
|
|
172
|
+
<div className='space-y-4 px-1 py-2'>
|
|
173
|
+
{Object.entries(schema.fields).map(([field, type]) => (
|
|
174
|
+
<FormField
|
|
175
|
+
key={field}
|
|
176
|
+
name={field}
|
|
177
|
+
type={type}
|
|
178
|
+
value={values[field]}
|
|
179
|
+
onChange={(v) => onChange(field, v)}
|
|
180
|
+
disabled={!isNew && schema.key.includes(field)}
|
|
181
|
+
isKey={schema.key.includes(field)}
|
|
182
|
+
/>
|
|
183
|
+
))}
|
|
184
|
+
</div>
|
|
185
|
+
</ScrollArea>
|
|
186
|
+
<DialogFooter className='flex-row gap-2'>
|
|
187
|
+
{!isNew && (
|
|
188
|
+
<Button
|
|
189
|
+
variant='destructive'
|
|
190
|
+
onClick={handleDelete}
|
|
191
|
+
disabled={deleting || saving}
|
|
192
|
+
className='mr-auto'
|
|
193
|
+
>
|
|
194
|
+
{confirmDelete ? 'Confirm Delete' : 'Delete'}
|
|
195
|
+
</Button>
|
|
196
|
+
)}
|
|
197
|
+
<Button
|
|
198
|
+
variant='outline'
|
|
199
|
+
onClick={() => onOpenChange(false)}
|
|
200
|
+
disabled={saving || deleting}
|
|
201
|
+
>
|
|
202
|
+
Cancel
|
|
203
|
+
</Button>
|
|
204
|
+
<Button onClick={handleSave} disabled={saving || deleting}>
|
|
205
|
+
{saving ? 'Saving...' : isNew ? 'Create' : 'Save'}
|
|
206
|
+
</Button>
|
|
207
|
+
</DialogFooter>
|
|
208
|
+
</DialogContent>
|
|
209
|
+
</Dialog>
|
|
210
|
+
)
|
|
211
|
+
}
|