@opensaas/stack-ui 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.
- package/.turbo/turbo-build.log +8 -0
- package/README.md +286 -0
- package/dist/components/AdminUI.d.ts +24 -0
- package/dist/components/AdminUI.d.ts.map +1 -0
- package/dist/components/AdminUI.js +48 -0
- package/dist/components/ConfirmDialog.d.ts +16 -0
- package/dist/components/ConfirmDialog.d.ts.map +1 -0
- package/dist/components/ConfirmDialog.js +11 -0
- package/dist/components/Dashboard.d.ts +12 -0
- package/dist/components/Dashboard.d.ts.map +1 -0
- package/dist/components/Dashboard.js +30 -0
- package/dist/components/ItemForm.d.ts +17 -0
- package/dist/components/ItemForm.d.ts.map +1 -0
- package/dist/components/ItemForm.js +97 -0
- package/dist/components/ItemFormClient.d.ts +22 -0
- package/dist/components/ItemFormClient.d.ts.map +1 -0
- package/dist/components/ItemFormClient.js +127 -0
- package/dist/components/ListView.d.ts +17 -0
- package/dist/components/ListView.d.ts.map +1 -0
- package/dist/components/ListView.js +76 -0
- package/dist/components/ListViewClient.d.ts +19 -0
- package/dist/components/ListViewClient.d.ts.map +1 -0
- package/dist/components/ListViewClient.js +108 -0
- package/dist/components/LoadingSpinner.d.ts +10 -0
- package/dist/components/LoadingSpinner.d.ts.map +1 -0
- package/dist/components/LoadingSpinner.js +14 -0
- package/dist/components/Navigation.d.ts +13 -0
- package/dist/components/Navigation.d.ts.map +1 -0
- package/dist/components/Navigation.js +20 -0
- package/dist/components/SkeletonLoader.d.ts +22 -0
- package/dist/components/SkeletonLoader.d.ts.map +1 -0
- package/dist/components/SkeletonLoader.js +25 -0
- package/dist/components/fields/CheckboxField.d.ts +11 -0
- package/dist/components/fields/CheckboxField.d.ts.map +1 -0
- package/dist/components/fields/CheckboxField.js +10 -0
- package/dist/components/fields/ComboboxField.d.ts +18 -0
- package/dist/components/fields/ComboboxField.d.ts.map +1 -0
- package/dist/components/fields/ComboboxField.js +32 -0
- package/dist/components/fields/FieldRenderer.d.ts +22 -0
- package/dist/components/fields/FieldRenderer.d.ts.map +1 -0
- package/dist/components/fields/FieldRenderer.js +81 -0
- package/dist/components/fields/IntegerField.d.ts +15 -0
- package/dist/components/fields/IntegerField.d.ts.map +1 -0
- package/dist/components/fields/IntegerField.js +14 -0
- package/dist/components/fields/PasswordField.d.ts +18 -0
- package/dist/components/fields/PasswordField.d.ts.map +1 -0
- package/dist/components/fields/PasswordField.js +42 -0
- package/dist/components/fields/RelationshipField.d.ts +20 -0
- package/dist/components/fields/RelationshipField.d.ts.map +1 -0
- package/dist/components/fields/RelationshipField.js +11 -0
- package/dist/components/fields/RelationshipManager.d.ts +19 -0
- package/dist/components/fields/RelationshipManager.d.ts.map +1 -0
- package/dist/components/fields/RelationshipManager.js +37 -0
- package/dist/components/fields/SelectField.d.ts +16 -0
- package/dist/components/fields/SelectField.d.ts.map +1 -0
- package/dist/components/fields/SelectField.js +11 -0
- package/dist/components/fields/TextField.d.ts +13 -0
- package/dist/components/fields/TextField.d.ts.map +1 -0
- package/dist/components/fields/TextField.js +11 -0
- package/dist/components/fields/TimestampField.d.ts +12 -0
- package/dist/components/fields/TimestampField.d.ts.map +1 -0
- package/dist/components/fields/TimestampField.js +12 -0
- package/dist/components/fields/index.d.ts +23 -0
- package/dist/components/fields/index.d.ts.map +1 -0
- package/dist/components/fields/index.js +13 -0
- package/dist/components/fields/registry.d.ts +43 -0
- package/dist/components/fields/registry.d.ts.map +1 -0
- package/dist/components/fields/registry.js +42 -0
- package/dist/components/standalone/DeleteButton.d.ts +35 -0
- package/dist/components/standalone/DeleteButton.d.ts.map +1 -0
- package/dist/components/standalone/DeleteButton.js +46 -0
- package/dist/components/standalone/ItemCreateForm.d.ts +34 -0
- package/dist/components/standalone/ItemCreateForm.d.ts.map +1 -0
- package/dist/components/standalone/ItemCreateForm.js +91 -0
- package/dist/components/standalone/ItemEditForm.d.ts +37 -0
- package/dist/components/standalone/ItemEditForm.d.ts.map +1 -0
- package/dist/components/standalone/ItemEditForm.js +112 -0
- package/dist/components/standalone/ListTable.d.ts +33 -0
- package/dist/components/standalone/ListTable.d.ts.map +1 -0
- package/dist/components/standalone/ListTable.js +94 -0
- package/dist/components/standalone/SearchBar.d.ts +29 -0
- package/dist/components/standalone/SearchBar.d.ts.map +1 -0
- package/dist/components/standalone/SearchBar.js +43 -0
- package/dist/components/standalone/index.d.ts +11 -0
- package/dist/components/standalone/index.d.ts.map +1 -0
- package/dist/components/standalone/index.js +6 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/lib/serializeFieldConfig.d.ts +43 -0
- package/dist/lib/serializeFieldConfig.d.ts.map +1 -0
- package/dist/lib/serializeFieldConfig.js +48 -0
- package/dist/lib/theme.d.ts +17 -0
- package/dist/lib/theme.d.ts.map +1 -0
- package/dist/lib/theme.js +192 -0
- package/dist/lib/utils.d.ts +18 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +76 -0
- package/dist/primitives/button.d.ts +12 -0
- package/dist/primitives/button.d.ts.map +1 -0
- package/dist/primitives/button.js +33 -0
- package/dist/primitives/calendar.d.ts +9 -0
- package/dist/primitives/calendar.d.ts.map +1 -0
- package/dist/primitives/calendar.js +48 -0
- package/dist/primitives/card.d.ts +9 -0
- package/dist/primitives/card.d.ts.map +1 -0
- package/dist/primitives/card.js +16 -0
- package/dist/primitives/checkbox.d.ts +5 -0
- package/dist/primitives/checkbox.d.ts.map +1 -0
- package/dist/primitives/checkbox.js +7 -0
- package/dist/primitives/combobox.d.ts +14 -0
- package/dist/primitives/combobox.d.ts.map +1 -0
- package/dist/primitives/combobox.js +20 -0
- package/dist/primitives/datetime-picker.d.ts +9 -0
- package/dist/primitives/datetime-picker.d.ts.map +1 -0
- package/dist/primitives/datetime-picker.js +42 -0
- package/dist/primitives/dialog.d.ts +20 -0
- package/dist/primitives/dialog.d.ts.map +1 -0
- package/dist/primitives/dialog.js +21 -0
- package/dist/primitives/index.d.ts +14 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +14 -0
- package/dist/primitives/input.d.ts +5 -0
- package/dist/primitives/input.d.ts.map +1 -0
- package/dist/primitives/input.js +8 -0
- package/dist/primitives/label.d.ts +6 -0
- package/dist/primitives/label.d.ts.map +1 -0
- package/dist/primitives/label.js +9 -0
- package/dist/primitives/popover.d.ts +7 -0
- package/dist/primitives/popover.d.ts.map +1 -0
- package/dist/primitives/popover.js +10 -0
- package/dist/primitives/select.d.ts +14 -0
- package/dist/primitives/select.d.ts.map +1 -0
- package/dist/primitives/select.js +24 -0
- package/dist/primitives/table.d.ts +11 -0
- package/dist/primitives/table.d.ts.map +1 -0
- package/dist/primitives/table.js +20 -0
- package/dist/primitives/time-picker.d.ts +8 -0
- package/dist/primitives/time-picker.d.ts.map +1 -0
- package/dist/primitives/time-picker.js +27 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2 -0
- package/dist/server/types.d.ts +15 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +1 -0
- package/dist/styles/globals.css +1896 -0
- package/package.json +91 -0
- package/postcss.config.cjs +5 -0
- package/src/components/AdminUI.tsx +112 -0
- package/src/components/ConfirmDialog.tsx +56 -0
- package/src/components/Dashboard.tsx +134 -0
- package/src/components/ItemForm.tsx +195 -0
- package/src/components/ItemFormClient.tsx +237 -0
- package/src/components/ListView.tsx +153 -0
- package/src/components/ListViewClient.tsx +282 -0
- package/src/components/LoadingSpinner.tsx +32 -0
- package/src/components/Navigation.tsx +117 -0
- package/src/components/SkeletonLoader.tsx +82 -0
- package/src/components/fields/CheckboxField.tsx +54 -0
- package/src/components/fields/ComboboxField.tsx +127 -0
- package/src/components/fields/FieldRenderer.tsx +132 -0
- package/src/components/fields/IntegerField.tsx +68 -0
- package/src/components/fields/PasswordField.tsx +159 -0
- package/src/components/fields/RelationshipField.tsx +71 -0
- package/src/components/fields/RelationshipManager.tsx +189 -0
- package/src/components/fields/SelectField.tsx +71 -0
- package/src/components/fields/TextField.tsx +59 -0
- package/src/components/fields/TimestampField.tsx +49 -0
- package/src/components/fields/index.ts +27 -0
- package/src/components/fields/registry.ts +72 -0
- package/src/components/standalone/DeleteButton.tsx +114 -0
- package/src/components/standalone/ItemCreateForm.tsx +161 -0
- package/src/components/standalone/ItemEditForm.tsx +193 -0
- package/src/components/standalone/ListTable.tsx +211 -0
- package/src/components/standalone/SearchBar.tsx +86 -0
- package/src/components/standalone/index.ts +13 -0
- package/src/index.ts +74 -0
- package/src/lib/serializeFieldConfig.ts +88 -0
- package/src/lib/theme.ts +202 -0
- package/src/lib/utils.ts +81 -0
- package/src/primitives/button.tsx +49 -0
- package/src/primitives/calendar.tsx +160 -0
- package/src/primitives/card.tsx +58 -0
- package/src/primitives/checkbox.tsx +27 -0
- package/src/primitives/combobox.tsx +159 -0
- package/src/primitives/datetime-picker.tsx +130 -0
- package/src/primitives/dialog.tsx +108 -0
- package/src/primitives/index.ts +54 -0
- package/src/primitives/input.tsx +24 -0
- package/src/primitives/label.tsx +19 -0
- package/src/primitives/popover.tsx +31 -0
- package/src/primitives/select.tsx +158 -0
- package/src/primitives/table.tsx +91 -0
- package/src/primitives/time-picker.tsx +65 -0
- package/src/server/index.ts +3 -0
- package/src/server/types.ts +15 -0
- package/src/styles/globals.css +123 -0
- package/tailwind.config.ts +3 -0
- package/tests/components/TextField.test.tsx +94 -0
- package/tests/setup.ts +11 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# @opensaas/stack-ui
|
|
2
|
+
|
|
3
|
+
Composable React UI components for OpenSaas Stack, built with Radix UI and shadcn/ui.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @opensaas/stack-ui
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- 🎨 **Four Levels of Abstraction** - Primitives, Fields, Standalone Components, Full Admin UI
|
|
14
|
+
- ♿ **Accessible** - Built with Radix UI primitives
|
|
15
|
+
- 🎯 **Type-Safe** - Full TypeScript support
|
|
16
|
+
- 🎨 **Customizable** - Tailwind CSS v4 with CSS variables
|
|
17
|
+
- 📦 **Tree-Shakeable** - Import only what you need
|
|
18
|
+
- 🧩 **Composable** - Mix and match components
|
|
19
|
+
|
|
20
|
+
## Package Exports
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// Primitives (shadcn/ui components)
|
|
24
|
+
import { Button, Input, Card, Table, Dialog } from '@opensaas/stack-ui/primitives'
|
|
25
|
+
|
|
26
|
+
// Field components (OpenSaas-aware)
|
|
27
|
+
import { TextField, SelectField, RelationshipField } from '@opensaas/stack-ui/fields'
|
|
28
|
+
|
|
29
|
+
// Standalone components (complete features)
|
|
30
|
+
import { ItemCreateForm, ListTable, SearchBar } from '@opensaas/stack-ui/standalone'
|
|
31
|
+
|
|
32
|
+
// Full components (page-level)
|
|
33
|
+
import { Dashboard, ListView, ItemForm, AdminUI } from '@opensaas/stack-ui'
|
|
34
|
+
|
|
35
|
+
// Server utilities
|
|
36
|
+
import { getAdminContext } from '@opensaas/stack-ui/server'
|
|
37
|
+
|
|
38
|
+
// Utility functions
|
|
39
|
+
import { cn, formatListName, formatFieldName } from '@opensaas/stack-ui/lib/utils'
|
|
40
|
+
|
|
41
|
+
// Styles
|
|
42
|
+
import '@opensaas/stack-ui/styles'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Architecture
|
|
46
|
+
|
|
47
|
+
### Level 1: Primitives (`@opensaas/stack-ui/primitives`)
|
|
48
|
+
|
|
49
|
+
Low-level UI components based on Radix UI and shadcn/ui.
|
|
50
|
+
|
|
51
|
+
**Available Components:**
|
|
52
|
+
|
|
53
|
+
- Button - Buttons with variants
|
|
54
|
+
- Input - Text inputs
|
|
55
|
+
- Label - Form labels
|
|
56
|
+
- Card - Content containers
|
|
57
|
+
- Table - Data tables
|
|
58
|
+
- Dialog - Modal dialogs
|
|
59
|
+
- Select - Dropdown selects
|
|
60
|
+
- Checkbox - Checkboxes
|
|
61
|
+
|
|
62
|
+
**Example:**
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { Button, Card, CardHeader, CardTitle, CardContent } from '@opensaas/stack-ui/primitives'
|
|
66
|
+
;<Card>
|
|
67
|
+
<CardHeader>
|
|
68
|
+
<CardTitle>Welcome</CardTitle>
|
|
69
|
+
</CardHeader>
|
|
70
|
+
<CardContent>
|
|
71
|
+
<Button>Get Started</Button>
|
|
72
|
+
</CardContent>
|
|
73
|
+
</Card>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Level 2: Fields (`@opensaas/stack-ui/fields`)
|
|
77
|
+
|
|
78
|
+
OpenSaas-aware form fields with validation and access control.
|
|
79
|
+
|
|
80
|
+
**Available Fields:**
|
|
81
|
+
|
|
82
|
+
- TextField
|
|
83
|
+
- IntegerField
|
|
84
|
+
- CheckboxField
|
|
85
|
+
- SelectField
|
|
86
|
+
- PasswordField
|
|
87
|
+
- TimestampField
|
|
88
|
+
- RelationshipField
|
|
89
|
+
|
|
90
|
+
**Example:**
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { TextField, SelectField } from '@opensaas/stack-ui/fields'
|
|
94
|
+
;<form>
|
|
95
|
+
<TextField name="email" label="Email" value={email} onChange={setEmail} required />
|
|
96
|
+
<SelectField
|
|
97
|
+
name="role"
|
|
98
|
+
label="Role"
|
|
99
|
+
value={role}
|
|
100
|
+
onChange={setRole}
|
|
101
|
+
options={[
|
|
102
|
+
{ label: 'Admin', value: 'admin' },
|
|
103
|
+
{ label: 'User', value: 'user' },
|
|
104
|
+
]}
|
|
105
|
+
/>
|
|
106
|
+
</form>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Level 3: Standalone Components (`@opensaas/stack-ui/standalone`)
|
|
110
|
+
|
|
111
|
+
Complete, reusable components for common admin tasks.
|
|
112
|
+
|
|
113
|
+
**Available Components:**
|
|
114
|
+
|
|
115
|
+
#### ItemCreateForm
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import { ItemCreateForm } from '@opensaas/stack-ui/standalone'
|
|
119
|
+
;<ItemCreateForm
|
|
120
|
+
fields={config.lists.Post.fields}
|
|
121
|
+
onSubmit={async (data) => {
|
|
122
|
+
const post = await createPost(data)
|
|
123
|
+
return { success: !!post }
|
|
124
|
+
}}
|
|
125
|
+
onCancel={() => router.back()}
|
|
126
|
+
/>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### ItemEditForm
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import { ItemEditForm } from '@opensaas/stack-ui/standalone'
|
|
133
|
+
;<ItemEditForm
|
|
134
|
+
fields={config.lists.Post.fields}
|
|
135
|
+
initialData={post}
|
|
136
|
+
onSubmit={async (data) => {
|
|
137
|
+
const updated = await updatePost(post.id, data)
|
|
138
|
+
return { success: !!updated }
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### ListTable
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
import { ListTable } from '@opensaas/stack-ui/standalone'
|
|
147
|
+
;<ListTable
|
|
148
|
+
items={posts}
|
|
149
|
+
fieldTypes={{ title: 'text', status: 'select' }}
|
|
150
|
+
columns={['title', 'status']}
|
|
151
|
+
onRowClick={(post) => router.push(`/posts/${post.id}`)}
|
|
152
|
+
sortable
|
|
153
|
+
/>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### SearchBar
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
import { SearchBar } from '@opensaas/stack-ui/standalone'
|
|
160
|
+
;<SearchBar onSearch={(query) => fetchPosts({ search: query })} placeholder="Search posts..." />
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### DeleteButton
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
import { DeleteButton } from '@opensaas/stack-ui/standalone'
|
|
167
|
+
;<DeleteButton
|
|
168
|
+
onDelete={async () => {
|
|
169
|
+
await deletePost(postId)
|
|
170
|
+
return { success: true }
|
|
171
|
+
}}
|
|
172
|
+
itemName="post"
|
|
173
|
+
/>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Level 4: Full Admin UI (`@opensaas/stack-ui`)
|
|
177
|
+
|
|
178
|
+
Complete admin interface with routing and navigation.
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
import { AdminUI } from '@opensaas/stack-ui'
|
|
182
|
+
;<AdminUI
|
|
183
|
+
context={context}
|
|
184
|
+
params={params?.admin}
|
|
185
|
+
searchParams={searchParams}
|
|
186
|
+
basePath="/admin"
|
|
187
|
+
serverAction={handleServerAction}
|
|
188
|
+
/>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Component Registry
|
|
192
|
+
|
|
193
|
+
Extend or override field components:
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
import { registerFieldComponent } from '@opensaas/stack-ui'
|
|
197
|
+
import { ColorPickerField } from './components/ColorPickerField'
|
|
198
|
+
|
|
199
|
+
// Register globally
|
|
200
|
+
registerFieldComponent('color', ColorPickerField)
|
|
201
|
+
|
|
202
|
+
// Use in config
|
|
203
|
+
fields: {
|
|
204
|
+
themeColor: text({
|
|
205
|
+
ui: { fieldType: 'color' },
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Or override per-field
|
|
210
|
+
fields: {
|
|
211
|
+
slug: text({
|
|
212
|
+
ui: { component: SlugField },
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Theming
|
|
218
|
+
|
|
219
|
+
All components use Tailwind CSS v4 with CSS variables:
|
|
220
|
+
|
|
221
|
+
```css
|
|
222
|
+
/* app/globals.css */
|
|
223
|
+
@import '@opensaas/stack-ui/styles';
|
|
224
|
+
|
|
225
|
+
:root {
|
|
226
|
+
--background: 0 0% 100%;
|
|
227
|
+
--foreground: 0 0% 3.9%;
|
|
228
|
+
--primary: 0 0% 9%;
|
|
229
|
+
--primary-foreground: 0 0% 98%;
|
|
230
|
+
--destructive: 0 84.2% 60.2%;
|
|
231
|
+
--destructive-foreground: 0 0% 98%;
|
|
232
|
+
--muted: 0 0% 96.1%;
|
|
233
|
+
--muted-foreground: 0 0% 45.1%;
|
|
234
|
+
--accent: 0 0% 96.1%;
|
|
235
|
+
--accent-foreground: 0 0% 9%;
|
|
236
|
+
--border: 0 0% 89.8%;
|
|
237
|
+
--input: 0 0% 89.8%;
|
|
238
|
+
--ring: 0 0% 3.9%;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.dark {
|
|
242
|
+
--background: 0 0% 3.9%;
|
|
243
|
+
--foreground: 0 0% 98%;
|
|
244
|
+
/* ... */
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Accessibility
|
|
249
|
+
|
|
250
|
+
All components follow WAI-ARIA guidelines:
|
|
251
|
+
|
|
252
|
+
- Proper semantic HTML
|
|
253
|
+
- ARIA attributes
|
|
254
|
+
- Keyboard navigation
|
|
255
|
+
- Focus management
|
|
256
|
+
- Screen reader support
|
|
257
|
+
|
|
258
|
+
## TypeScript
|
|
259
|
+
|
|
260
|
+
Full TypeScript support with exported types:
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import type {
|
|
264
|
+
FieldComponent,
|
|
265
|
+
FieldComponentProps,
|
|
266
|
+
ItemCreateFormProps,
|
|
267
|
+
ListTableProps,
|
|
268
|
+
AdminUIProps,
|
|
269
|
+
} from '@opensaas/stack-ui'
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Examples
|
|
273
|
+
|
|
274
|
+
- [Blog Example](../../examples/blog) - Basic usage with full AdminUI
|
|
275
|
+
- [Custom Field Example](../../examples/custom-field) - Custom field components
|
|
276
|
+
- [Composable Dashboard](../../examples/composable-dashboard) - Using standalone components
|
|
277
|
+
|
|
278
|
+
## Learn More
|
|
279
|
+
|
|
280
|
+
- [Composability Guide](../../docs/COMPOSABILITY.md) - Complete guide to all four levels
|
|
281
|
+
- [API Reference](../../docs/API.md) - Full API documentation
|
|
282
|
+
- [OpenSaas Stack](../../README.md) - Stack overview
|
|
283
|
+
|
|
284
|
+
## License
|
|
285
|
+
|
|
286
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ServerActionInput } from '../server/types.js';
|
|
2
|
+
import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
|
|
3
|
+
export interface AdminUIProps<TPrisma> {
|
|
4
|
+
context: AccessContext<TPrisma>;
|
|
5
|
+
config: OpenSaasConfig;
|
|
6
|
+
params?: string[];
|
|
7
|
+
searchParams?: {
|
|
8
|
+
[key: string]: string | string[] | undefined;
|
|
9
|
+
};
|
|
10
|
+
basePath?: string;
|
|
11
|
+
serverAction: (input: ServerActionInput) => Promise<unknown>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Main AdminUI component - complete admin interface with routing
|
|
15
|
+
* Server Component
|
|
16
|
+
*
|
|
17
|
+
* Handles routing based on params array:
|
|
18
|
+
* - [] → Dashboard
|
|
19
|
+
* - [list] → ListView
|
|
20
|
+
* - [list, 'create'] → ItemForm (create)
|
|
21
|
+
* - [list, id] → ItemForm (edit)
|
|
22
|
+
*/
|
|
23
|
+
export declare function AdminUI<TPrisma>({ context, config, params, searchParams, basePath, serverAction, }: AdminUIProps<TPrisma>): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
//# sourceMappingURL=AdminUI.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AdminUI.d.ts","sourceRoot":"","sources":["../../src/components/AdminUI.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAqB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGvF,MAAM,WAAW,YAAY,CAAC,OAAO;IACnC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,YAAY,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAA;KAAE,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC7D;AAED;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,EAC/B,OAAO,EACP,MAAM,EACN,MAAW,EACX,YAAiB,EACjB,QAAmB,EACnB,YAAY,GACb,EAAE,YAAY,CAAC,OAAO,CAAC,2CA2EvB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Navigation } from './Navigation.js';
|
|
3
|
+
import { Dashboard } from './Dashboard.js';
|
|
4
|
+
import { ListView } from './ListView.js';
|
|
5
|
+
import { ItemForm } from './ItemForm.js';
|
|
6
|
+
import { getListKeyFromUrl } from '@opensaas/stack-core';
|
|
7
|
+
import { generateThemeCSS } from '../lib/theme.js';
|
|
8
|
+
/**
|
|
9
|
+
* Main AdminUI component - complete admin interface with routing
|
|
10
|
+
* Server Component
|
|
11
|
+
*
|
|
12
|
+
* Handles routing based on params array:
|
|
13
|
+
* - [] → Dashboard
|
|
14
|
+
* - [list] → ListView
|
|
15
|
+
* - [list, 'create'] → ItemForm (create)
|
|
16
|
+
* - [list, id] → ItemForm (edit)
|
|
17
|
+
*/
|
|
18
|
+
export function AdminUI({ context, config, params = [], searchParams = {}, basePath = '/admin', serverAction, }) {
|
|
19
|
+
// Parse route from params
|
|
20
|
+
const [urlSegment, action] = params;
|
|
21
|
+
// Convert URL segment (kebab-case) to PascalCase listKey
|
|
22
|
+
const listKey = urlSegment ? getListKeyFromUrl(urlSegment) : undefined;
|
|
23
|
+
// Determine current path for navigation highlighting
|
|
24
|
+
const currentPath = params.length > 0 ? `/${params.join('/')}` : '';
|
|
25
|
+
// Route to appropriate component
|
|
26
|
+
let content;
|
|
27
|
+
if (!listKey) {
|
|
28
|
+
// Dashboard
|
|
29
|
+
content = _jsx(Dashboard, { context: context, config: config, basePath: basePath });
|
|
30
|
+
}
|
|
31
|
+
else if (action === 'create') {
|
|
32
|
+
// Create form
|
|
33
|
+
content = (_jsx(ItemForm, { context: context, config: config, listKey: listKey, mode: "create", basePath: basePath, serverAction: serverAction }));
|
|
34
|
+
}
|
|
35
|
+
else if (action && action !== 'create') {
|
|
36
|
+
// Edit form (action is the item ID)
|
|
37
|
+
content = (_jsx(ItemForm, { context: context, config: config, listKey: listKey, mode: "edit", itemId: action, basePath: basePath, serverAction: serverAction }));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// List view
|
|
41
|
+
const search = typeof searchParams.search === 'string' ? searchParams.search : undefined;
|
|
42
|
+
const page = typeof searchParams.page === 'string' ? parseInt(searchParams.page, 10) : 1;
|
|
43
|
+
content = (_jsx(ListView, { context: context, config: config, listKey: listKey, basePath: basePath, search: search, page: page }));
|
|
44
|
+
}
|
|
45
|
+
// Generate theme styles if custom theme is configured
|
|
46
|
+
const themeStyles = config.ui?.theme ? generateThemeCSS(config.ui.theme) : null;
|
|
47
|
+
return (_jsxs(_Fragment, { children: [themeStyles && _jsx("style", { dangerouslySetInnerHTML: { __html: themeStyles } }), _jsxs("div", { className: "flex min-h-screen bg-background", children: [_jsx(Navigation, { context: context, config: config, basePath: basePath, currentPath: currentPath }), _jsx("main", { className: "flex-1 overflow-y-auto", children: content })] })] }));
|
|
48
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ConfirmDialogProps {
|
|
2
|
+
isOpen: boolean;
|
|
3
|
+
title: string;
|
|
4
|
+
message: string;
|
|
5
|
+
confirmLabel?: string;
|
|
6
|
+
cancelLabel?: string;
|
|
7
|
+
onConfirm: () => void;
|
|
8
|
+
onCancel: () => void;
|
|
9
|
+
variant?: 'danger' | 'warning';
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Reusable confirmation dialog component
|
|
13
|
+
* Used for destructive actions like delete
|
|
14
|
+
*/
|
|
15
|
+
export declare function ConfirmDialog({ isOpen, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, variant, }: ConfirmDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=ConfirmDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConfirmDialog.d.ts","sourceRoot":"","sources":["../../src/components/ConfirmDialog.tsx"],"names":[],"mappings":"AAYA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,OAAO,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;CAC/B;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,KAAK,EACL,OAAO,EACP,YAAwB,EACxB,WAAsB,EACtB,SAAS,EACT,QAAQ,EACR,OAAkB,GACnB,EAAE,kBAAkB,2CAmBpB"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '../primitives/dialog.js';
|
|
4
|
+
import { Button } from '../primitives/button.js';
|
|
5
|
+
/**
|
|
6
|
+
* Reusable confirmation dialog component
|
|
7
|
+
* Used for destructive actions like delete
|
|
8
|
+
*/
|
|
9
|
+
export function ConfirmDialog({ isOpen, title, message, confirmLabel = 'Confirm', cancelLabel = 'Cancel', onConfirm, onCancel, variant = 'danger', }) {
|
|
10
|
+
return (_jsx(Dialog, { open: isOpen, onOpenChange: (open) => !open && onCancel(), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: message })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: onCancel, children: cancelLabel }), _jsx(Button, { variant: variant === 'danger' ? 'destructive' : 'default', onClick: onConfirm, children: confirmLabel })] })] }) }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
|
|
2
|
+
export interface DashboardProps<TPrisma> {
|
|
3
|
+
context: AccessContext<TPrisma>;
|
|
4
|
+
config: OpenSaasConfig;
|
|
5
|
+
basePath?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Dashboard landing page showing all available lists
|
|
9
|
+
* Server Component
|
|
10
|
+
*/
|
|
11
|
+
export declare function Dashboard<TPrisma>({ context, config, basePath, }: DashboardProps<TPrisma>): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
12
|
+
//# sourceMappingURL=Dashboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Dashboard.d.ts","sourceRoot":"","sources":["../../src/components/Dashboard.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAuB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGzF,MAAM,WAAW,cAAc,CAAC,OAAO;IACrC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,EACvC,OAAO,EACP,MAAM,EACN,QAAmB,GACpB,EAAE,cAAc,CAAC,OAAO,CAAC,oDAkHzB"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Link from 'next/link';
|
|
3
|
+
import { formatListName } from '../lib/utils.js';
|
|
4
|
+
import { getDbKey, getUrlKey } from '@opensaas/stack-core';
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../primitives/card.js';
|
|
6
|
+
/**
|
|
7
|
+
* Dashboard landing page showing all available lists
|
|
8
|
+
* Server Component
|
|
9
|
+
*/
|
|
10
|
+
export async function Dashboard({ context, config, basePath = '/admin', }) {
|
|
11
|
+
const lists = Object.keys(config.lists || {});
|
|
12
|
+
// Get counts for each list
|
|
13
|
+
const listCounts = await Promise.all(lists.map(async (listKey) => {
|
|
14
|
+
try {
|
|
15
|
+
const count = await context.db[getDbKey(listKey)]?.count();
|
|
16
|
+
return { listKey, count: count || 0 };
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.error(`Failed to get count for ${listKey}:`, error);
|
|
20
|
+
return { listKey, count: 0 };
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
return (_jsxs("div", { className: "p-8", children: [_jsxs("div", { className: "mb-8 relative", children: [_jsx("div", { className: "absolute inset-0 bg-gradient-to-r from-primary/5 to-accent/5 opacity-100 rounded-2xl" }), _jsxs("div", { className: "relative p-6", children: [_jsx("h1", { className: "text-4xl font-bold mb-2 bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent", children: "Dashboard" }), _jsx("p", { className: "text-muted-foreground", children: "Manage your application data" })] })] }), lists.length === 0 ? (_jsxs(Card, { className: "p-12 text-center border-2 border-dashed", children: [_jsx("div", { className: "mb-4 text-4xl", children: "\uD83D\uDCE6" }), _jsx("p", { className: "text-muted-foreground mb-2 font-medium", children: "No lists configured" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Add lists to your opensaas.config.ts to get started." })] })) : (_jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6", children: listCounts.map(({ listKey, count }) => {
|
|
24
|
+
const urlKey = getUrlKey(listKey);
|
|
25
|
+
return (_jsx(Link, { href: `${basePath}/${urlKey}`, children: _jsxs(Card, { className: "group hover:border-primary hover:shadow-lg hover:shadow-primary/20 transition-all duration-200 cursor-pointer h-full relative overflow-hidden", children: [_jsx("div", { className: "absolute inset-0 bg-gradient-to-br from-primary/5 to-accent/5 opacity-0 group-hover:opacity-100 transition-opacity" }), _jsx(CardHeader, { className: "relative", children: _jsxs("div", { className: "flex items-start justify-between", children: [_jsxs("div", { children: [_jsx(CardTitle, { className: "text-xl group-hover:text-primary transition-colors", children: formatListName(listKey) }), _jsxs("p", { className: "text-sm text-muted-foreground mt-1 font-medium", children: [count, " ", count === 1 ? 'item' : 'items'] })] }), _jsx("div", { className: "text-3xl opacity-60 group-hover:opacity-100 transition-opacity", children: "\uD83D\uDCCB" })] }) }), _jsx(CardContent, { className: "relative", children: _jsxs("div", { className: "flex items-center text-sm font-medium text-primary", children: [_jsx("span", { children: "View all" }), _jsx("svg", { className: "ml-1 w-4 h-4 group-hover:translate-x-1 transition-transform", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) })] }) })] }) }, listKey));
|
|
26
|
+
}) })), lists.length > 0 && (_jsxs(Card, { className: "mt-12 bg-gradient-to-br from-accent/10 to-primary/10 border-accent/20", children: [_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "text-lg flex items-center gap-2", children: [_jsx("span", { className: "text-xl", children: "\u26A1" }), "Quick Actions"] }) }), _jsx(CardContent, { children: _jsx("div", { className: "flex flex-wrap gap-3", children: lists.map((listKey) => {
|
|
27
|
+
const urlKey = getUrlKey(listKey);
|
|
28
|
+
return (_jsxs(Link, { href: `${basePath}/${urlKey}/create`, className: "inline-flex items-center gap-1 px-4 py-2 rounded-lg bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground font-medium text-sm transition-colors border border-primary/20", children: [_jsx("span", { className: "text-lg", children: "+" }), "Create ", formatListName(listKey)] }, listKey));
|
|
29
|
+
}) }) })] }))] }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ServerActionInput } from '../server/types.js';
|
|
2
|
+
import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
|
|
3
|
+
export interface ItemFormProps<TPrisma> {
|
|
4
|
+
context: AccessContext<TPrisma>;
|
|
5
|
+
config: OpenSaasConfig;
|
|
6
|
+
listKey: string;
|
|
7
|
+
mode: 'create' | 'edit';
|
|
8
|
+
itemId?: string;
|
|
9
|
+
basePath?: string;
|
|
10
|
+
serverAction: (input: ServerActionInput) => Promise<unknown>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Item form component - create or edit an item
|
|
14
|
+
* Server Component that fetches data and sets up actions
|
|
15
|
+
*/
|
|
16
|
+
export declare function ItemForm<TPrisma>({ context, config, listKey, mode, itemId, basePath, serverAction, }: ItemFormProps<TPrisma>): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
17
|
+
//# sourceMappingURL=ItemForm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ItemForm.d.ts","sourceRoot":"","sources":["../../src/components/ItemForm.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAuB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGzF,MAAM,WAAW,aAAa,CAAC,OAAO;IACpC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC7D;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,EACtC,OAAO,EACP,MAAM,EACN,OAAO,EACP,IAAI,EACJ,MAAM,EACN,QAAmB,EACnB,YAAY,GACb,EAAE,aAAa,CAAC,OAAO,CAAC,oDAmKxB"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Link from 'next/link';
|
|
3
|
+
import { ItemFormClient } from './ItemFormClient.js';
|
|
4
|
+
import { formatListName } from '../lib/utils.js';
|
|
5
|
+
import { getDbKey, getUrlKey } from '@opensaas/stack-core';
|
|
6
|
+
import { serializeFieldConfigs } from '../lib/serializeFieldConfig.js';
|
|
7
|
+
/**
|
|
8
|
+
* Item form component - create or edit an item
|
|
9
|
+
* Server Component that fetches data and sets up actions
|
|
10
|
+
*/
|
|
11
|
+
export async function ItemForm({ context, config, listKey, mode, itemId, basePath = '/admin', serverAction, }) {
|
|
12
|
+
const listConfig = config.lists[listKey];
|
|
13
|
+
const urlKey = getUrlKey(listKey);
|
|
14
|
+
if (!listConfig) {
|
|
15
|
+
return (_jsx("div", { className: "p-8", children: _jsxs("div", { className: "bg-destructive/10 border border-destructive text-destructive rounded-lg p-6", children: [_jsx("h2", { className: "text-lg font-semibold mb-2", children: "List not found" }), _jsxs("p", { children: ["The list \"", listKey, "\" does not exist in your configuration."] })] }) }));
|
|
16
|
+
}
|
|
17
|
+
// Fetch item data if in edit mode
|
|
18
|
+
let itemData = {};
|
|
19
|
+
if (mode === 'edit' && itemId) {
|
|
20
|
+
try {
|
|
21
|
+
// Build include object for relationships
|
|
22
|
+
const includeRelationships = {};
|
|
23
|
+
for (const [fieldName, fieldConfig] of Object.entries(listConfig.fields)) {
|
|
24
|
+
const fieldConfigAny = fieldConfig;
|
|
25
|
+
if (fieldConfigAny.type === 'relationship') {
|
|
26
|
+
includeRelationships[fieldName] = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Fetch item with relationships included
|
|
30
|
+
itemData = await context.db[getDbKey(listKey)].findUnique({
|
|
31
|
+
where: { id: itemId },
|
|
32
|
+
...(Object.keys(includeRelationships).length > 0 && { include: includeRelationships }),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error(`Failed to fetch item ${itemId}:`, error);
|
|
37
|
+
}
|
|
38
|
+
if (!itemData) {
|
|
39
|
+
return (_jsx("div", { className: "p-8", children: _jsxs("div", { className: "bg-destructive/10 border border-destructive text-destructive rounded-lg p-6", children: [_jsx("h2", { className: "text-lg font-semibold mb-2", children: "Item not found" }), _jsx("p", { children: "The item you're trying to edit doesn't exist or you don't have access to it." }), _jsxs(Link, { href: `${basePath}/${urlKey}`, className: "inline-block mt-4 text-primary hover:underline", children: ["\u2190 Back to ", formatListName(listKey)] })] }) }));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Fetch relationship options for all relationship fields
|
|
43
|
+
const relationshipData = {};
|
|
44
|
+
for (const [fieldName, fieldConfig] of Object.entries(listConfig.fields)) {
|
|
45
|
+
// Check if field is a relationship type by checking the discriminated union
|
|
46
|
+
const fieldConfigAny = fieldConfig;
|
|
47
|
+
if (fieldConfigAny.type === 'relationship') {
|
|
48
|
+
const ref = fieldConfigAny.ref;
|
|
49
|
+
if (ref) {
|
|
50
|
+
// Parse ref format: "ListName.fieldName"
|
|
51
|
+
const relatedListName = ref.split('.')[0];
|
|
52
|
+
const relatedListConfig = config.lists[relatedListName];
|
|
53
|
+
if (relatedListConfig) {
|
|
54
|
+
try {
|
|
55
|
+
const dbContext = context.db;
|
|
56
|
+
const relatedItems = await dbContext[getDbKey(relatedListName)].findMany({});
|
|
57
|
+
// Use 'name' field as label if it exists, otherwise use 'id'
|
|
58
|
+
relationshipData[fieldName] = relatedItems.map((item) => ({
|
|
59
|
+
id: item.id,
|
|
60
|
+
label: (item.name || item.title || item.id) || '',
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error(`Failed to fetch relationship items for ${fieldName}:`, error);
|
|
65
|
+
relationshipData[fieldName] = [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Serialize field configs to remove non-serializable properties
|
|
72
|
+
const serializableFields = serializeFieldConfigs(listConfig.fields);
|
|
73
|
+
// Transform relationship data in itemData from objects to IDs for form
|
|
74
|
+
// Also apply valueForClientSerialization transformation
|
|
75
|
+
const formData = { ...itemData };
|
|
76
|
+
for (const [fieldName, fieldConfig] of Object.entries(listConfig.fields)) {
|
|
77
|
+
const fieldConfigAny = fieldConfig;
|
|
78
|
+
if (fieldConfigAny.type === 'relationship' && formData[fieldName]) {
|
|
79
|
+
const value = formData[fieldName];
|
|
80
|
+
if (fieldConfigAny.many && Array.isArray(value)) {
|
|
81
|
+
// Many relationship: extract IDs from array of objects
|
|
82
|
+
formData[fieldName] = value.map((item) => item.id);
|
|
83
|
+
}
|
|
84
|
+
else if (value && typeof value === 'object' && 'id' in value) {
|
|
85
|
+
// Single relationship: extract ID from object
|
|
86
|
+
formData[fieldName] = value.id;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Apply valueForClientSerialization if defined
|
|
90
|
+
if (fieldConfigAny.ui?.valueForClientSerialization &&
|
|
91
|
+
typeof fieldConfigAny.ui.valueForClientSerialization === 'function') {
|
|
92
|
+
const transformer = fieldConfigAny.ui.valueForClientSerialization;
|
|
93
|
+
formData[fieldName] = transformer({ value: formData[fieldName] });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return (_jsxs("div", { className: "p-8 max-w-4xl", children: [_jsxs("div", { className: "mb-8", children: [_jsxs(Link, { href: `${basePath}/${urlKey}`, className: "inline-flex items-center text-sm text-muted-foreground hover:text-foreground mb-4", children: [_jsx("svg", { className: "w-4 h-4 mr-1", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }), "Back to ", formatListName(listKey)] }), _jsxs("h1", { className: "text-3xl font-bold", children: [mode === 'create' ? 'Create' : 'Edit', " ", formatListName(listKey)] })] }), _jsx("div", { className: "bg-card border border-border rounded-lg p-6", children: _jsx(ItemFormClient, { listKey: listKey, urlKey: urlKey, mode: mode, fields: serializableFields, initialData: JSON.parse(JSON.stringify(formData)), itemId: itemId, basePath: basePath, serverAction: serverAction, relationshipData: relationshipData }) })] }));
|
|
97
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ServerActionInput } from '../server/types.js';
|
|
2
|
+
import type { SerializableFieldConfig } from '../lib/serializeFieldConfig.js';
|
|
3
|
+
export interface ItemFormClientProps {
|
|
4
|
+
listKey: string;
|
|
5
|
+
urlKey: string;
|
|
6
|
+
mode: 'create' | 'edit';
|
|
7
|
+
fields: Record<string, SerializableFieldConfig>;
|
|
8
|
+
initialData?: Record<string, unknown>;
|
|
9
|
+
itemId?: string;
|
|
10
|
+
basePath: string;
|
|
11
|
+
serverAction: (input: ServerActionInput) => Promise<unknown>;
|
|
12
|
+
relationshipData?: Record<string, Array<{
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
}>>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Client component for interactive form
|
|
19
|
+
* Handles form state, validation, and submission
|
|
20
|
+
*/
|
|
21
|
+
export declare function ItemFormClient({ listKey, urlKey, mode, fields, initialData, itemId, basePath, serverAction, relationshipData, }: ItemFormClientProps): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
//# sourceMappingURL=ItemFormClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ItemFormClient.d.ts","sourceRoot":"","sources":["../../src/components/ItemFormClient.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAA;AAE7E,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAA;IACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAA;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CACxE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,MAAM,EACN,IAAI,EACJ,MAAM,EACN,WAAgB,EAChB,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,gBAAqB,GACtB,EAAE,mBAAmB,2CAsMrB"}
|