@nestledjs/data-browser 0.1.1 → 0.1.4
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/index.d.ts +15 -0
- package/index.js +64 -0
- package/lib/admin-data.d.ts +2 -0
- package/lib/components/filters/DateRangeFilter.d.ts +7 -0
- package/lib/components/filters/DateRangeFilter.js +105 -0
- package/lib/components/filters/NumberRangeFilter.d.ts +8 -0
- package/lib/components/filters/NumberRangeFilter.js +115 -0
- package/lib/components/filters/RelationComponents.d.ts +32 -0
- package/lib/components/filters/RelationComponents.js +265 -0
- package/lib/components/filters/RelationFilterField.d.ts +8 -0
- package/lib/components/filters/RelationFilterField.js +116 -0
- package/lib/components/filters/index.d.ts +4 -0
- package/lib/components/index.d.ts +2 -0
- package/lib/components/shared/AdminBreadcrumbs.d.ts +13 -0
- package/lib/components/shared/AdminBreadcrumbs.js +103 -0
- package/lib/components/shared/AdminErrorStates.d.ts +30 -0
- package/lib/components/shared/AdminErrorStates.js +214 -0
- package/lib/components/shared/AdminStatusDisplay.d.ts +21 -0
- package/lib/components/shared/AdminStatusDisplay.js +205 -0
- package/lib/components/shared/index.d.ts +3 -0
- package/lib/context/AdminDataContext.d.ts +47 -0
- package/lib/context/AdminDataContext.js +28 -0
- package/lib/hooks/useAdminList.d.ts +5 -0
- package/lib/hooks/useAdminList.js +41 -0
- package/lib/hooks/useClickOutside.d.ts +2 -0
- package/lib/hooks/useClickOutside.js +13 -0
- package/lib/hooks/useDebounce.d.ts +1 -0
- package/lib/hooks/useDebounce.js +13 -0
- package/lib/hooks/useRelationData.d.ts +7 -0
- package/lib/hooks/useRelationData.js +64 -0
- package/lib/layouts/AdminDataLayout.d.ts +5 -0
- package/lib/layouts/AdminDataLayout.js +315 -0
- package/lib/pages/AdminDataCreatePage.d.ts +4 -0
- package/lib/pages/AdminDataCreatePage.js +487 -0
- package/lib/pages/AdminDataEditPage.d.ts +4 -0
- package/lib/pages/AdminDataEditPage.js +903 -0
- package/lib/pages/AdminDataIndexPage.d.ts +4 -0
- package/lib/pages/AdminDataIndexPage.js +91 -0
- package/lib/pages/AdminDataListPage.d.ts +9 -0
- package/lib/pages/AdminDataListPage.js +1218 -0
- package/lib/types/index.d.ts +60 -0
- package/lib/types/index.js +16 -0
- package/lib/utils/graphql-utils.d.ts +25 -0
- package/lib/utils/graphql-utils.js +265 -0
- package/lib/utils/secure-storage.d.ts +50 -0
- package/lib/utils/secure-storage.js +145 -0
- package/lib/utils/string-utils.d.ts +6 -0
- package/lib/utils/string-utils.js +27 -0
- package/package.json +16 -3
- package/.babelrc +0 -12
- package/LICENSE +0 -21
- package/eslint.config.mjs +0 -12
- package/project.json +0 -35
- package/src/index.ts +0 -28
- package/src/lib/admin-data.spec.tsx +0 -10
- package/src/lib/admin-data.tsx +0 -9
- package/src/lib/components/filters/DateRangeFilter.tsx +0 -88
- package/src/lib/components/filters/NumberRangeFilter.tsx +0 -111
- package/src/lib/components/filters/RelationComponents.tsx +0 -176
- package/src/lib/components/filters/RelationFilterField.tsx +0 -106
- package/src/lib/components/filters/index.ts +0 -5
- package/src/lib/components/index.ts +0 -2
- package/src/lib/components/shared/AdminBreadcrumbs.tsx +0 -88
- package/src/lib/components/shared/AdminErrorStates.tsx +0 -180
- package/src/lib/components/shared/AdminStatusDisplay.tsx +0 -292
- package/src/lib/components/shared/index.ts +0 -3
- package/src/lib/context/AdminDataContext.tsx +0 -74
- package/src/lib/hooks/useAdminList.ts +0 -42
- package/src/lib/hooks/useClickOutside.ts +0 -21
- package/src/lib/hooks/useDebounce.ts +0 -16
- package/src/lib/hooks/useRelationData.ts +0 -114
- package/src/lib/layouts/AdminDataLayout.tsx +0 -251
- package/src/lib/pages/AdminDataCreatePage.tsx +0 -415
- package/src/lib/pages/AdminDataEditPage.tsx +0 -777
- package/src/lib/pages/AdminDataIndexPage.tsx +0 -50
- package/src/lib/pages/AdminDataListPage.tsx +0 -921
- package/src/lib/types/index.ts +0 -51
- package/src/lib/utils/graphql-utils.ts +0 -538
- package/src/lib/utils/secure-storage.ts +0 -305
- package/src/lib/utils/string-utils.ts +0 -53
- package/tsconfig.json +0 -17
- package/tsconfig.lib.json +0 -20
- package/vite.config.ts +0 -35
|
@@ -1,415 +0,0 @@
|
|
|
1
|
-
import { gql } from '@apollo/client'
|
|
2
|
-
import { useMutation } from '@apollo/client/react'
|
|
3
|
-
import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/24/outline'
|
|
4
|
-
import { useAdminDataContext } from '../context/AdminDataContext'
|
|
5
|
-
|
|
6
|
-
function toReadableText(text: string): string {
|
|
7
|
-
return text.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, str => str.toUpperCase())
|
|
8
|
-
}
|
|
9
|
-
import { ErrorBoundary } from '@nestledjs/shared-components'
|
|
10
|
-
import { Form } from '@nestledjs/forms'
|
|
11
|
-
import { useEffect, useMemo, useState } from 'react'
|
|
12
|
-
import { Link, useNavigate, useParams } from 'react-router'
|
|
13
|
-
|
|
14
|
-
import { buildFormFields, cleanFormInput, getAdminDocuments } from '../utils/graphql-utils' // =================================
|
|
15
|
-
|
|
16
|
-
// =================================
|
|
17
|
-
// SECURITY UTILITIES
|
|
18
|
-
// =================================
|
|
19
|
-
|
|
20
|
-
// Sanitize and validate user input
|
|
21
|
-
function sanitizeInput(input: string | undefined): string {
|
|
22
|
-
if (!input || typeof input !== 'string') return ''
|
|
23
|
-
|
|
24
|
-
// Remove potentially dangerous characters and limit length
|
|
25
|
-
return input
|
|
26
|
-
.replace(/[<>"'%;()&+]/g, '') // Remove common injection characters
|
|
27
|
-
.replace(/javascript:/gi, '') // Remove javascript: protocols
|
|
28
|
-
.replace(/on\w+\s*=/gi, '') // Remove event handlers
|
|
29
|
-
.trim()
|
|
30
|
-
.substring(0, 100) // Limit length to prevent DoS
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Convert PascalCase to kebab-case for URLs (CourseChapter -> course-chapter)
|
|
34
|
-
const toKebabCase = (str: string): string => {
|
|
35
|
-
return str
|
|
36
|
-
.replace(/([a-z])([A-Z])/g, '$1-$2') // Insert dash between lowercase and uppercase
|
|
37
|
-
.toLowerCase() // Convert to lowercase
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Validation functions moved into component to access databaseModels from context
|
|
41
|
-
|
|
42
|
-
// Check if user has access to this data type (basic implementation)
|
|
43
|
-
function checkAccess(dataType: string): boolean {
|
|
44
|
-
// For now, allow access to all data types in admin
|
|
45
|
-
// In a real app, this would check user permissions
|
|
46
|
-
return true
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// =================================
|
|
50
|
-
// MAIN COMPONENT
|
|
51
|
-
// =================================
|
|
52
|
-
|
|
53
|
-
export function AdminDataCreatePage() {
|
|
54
|
-
const { dataType } = useParams()
|
|
55
|
-
const { databaseModels, basePath = '/admin/data', formTheme } = useAdminDataContext()
|
|
56
|
-
|
|
57
|
-
// Helper function to find model by name
|
|
58
|
-
const findModelByName = (name: string) => {
|
|
59
|
-
return databaseModels.find(model => model.name === name)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Validate data type against allowed models
|
|
63
|
-
const validateDataType = (dataType: string | undefined): string | null => {
|
|
64
|
-
const sanitized = sanitizeInput(dataType)
|
|
65
|
-
if (!sanitized) return null
|
|
66
|
-
|
|
67
|
-
// Convert kebab-case to PascalCase (course-chapter -> CourseChapter)
|
|
68
|
-
const properCaseDataType = sanitized
|
|
69
|
-
.split('-')
|
|
70
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
71
|
-
.join('')
|
|
72
|
-
|
|
73
|
-
// Check if this data type exists in our models
|
|
74
|
-
const model = databaseModels.find(m => m.name === properCaseDataType)
|
|
75
|
-
return model ? properCaseDataType : null
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Security validation
|
|
79
|
-
const validatedDataType = validateDataType(dataType)
|
|
80
|
-
|
|
81
|
-
// Determine what to render based on validation
|
|
82
|
-
const shouldShowUnauthorized = !validatedDataType
|
|
83
|
-
const shouldShowAccessDenied = validatedDataType && !checkAccess(validatedDataType)
|
|
84
|
-
const model = validatedDataType ? findModelByName(validatedDataType) : null
|
|
85
|
-
const shouldShowModelNotFound = validatedDataType && !shouldShowAccessDenied && !model
|
|
86
|
-
|
|
87
|
-
// Render error states
|
|
88
|
-
if (shouldShowUnauthorized) {
|
|
89
|
-
return (
|
|
90
|
-
<div className="flex flex-col justify-center py-12">
|
|
91
|
-
<div className="mt-8 mx-auto w-full max-w-md">
|
|
92
|
-
<div className="bg-white dark:bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
|
93
|
-
<div className="text-center">
|
|
94
|
-
<ExclamationCircleIcon className="mx-auto h-12 w-12 text-red-400" />
|
|
95
|
-
<h2 className="mt-4 text-lg font-medium text-gray-900 dark:text-gray-100">Unauthorized</h2>
|
|
96
|
-
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
97
|
-
Invalid data type or insufficient permissions.
|
|
98
|
-
</p>
|
|
99
|
-
<div className="mt-6">
|
|
100
|
-
<Link
|
|
101
|
-
to={basePath}
|
|
102
|
-
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-web hover:bg-green-web-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-web"
|
|
103
|
-
>
|
|
104
|
-
Return to Data Browser
|
|
105
|
-
</Link>
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (shouldShowAccessDenied) {
|
|
115
|
-
return (
|
|
116
|
-
<div className="flex flex-col justify-center py-12">
|
|
117
|
-
<div className="mt-8 mx-auto w-full max-w-md">
|
|
118
|
-
<div className="bg-white dark:bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
|
119
|
-
<div className="text-center">
|
|
120
|
-
<ExclamationCircleIcon className="mx-auto h-12 w-12 text-red-400" />
|
|
121
|
-
<h2 className="mt-4 text-lg font-medium text-gray-900 dark:text-gray-100">Access Denied</h2>
|
|
122
|
-
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
123
|
-
You don't have permission to create {toReadableText(validatedDataType!)} records.
|
|
124
|
-
</p>
|
|
125
|
-
<div className="mt-6">
|
|
126
|
-
<Link
|
|
127
|
-
to={basePath}
|
|
128
|
-
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-web hover:bg-green-web-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-web"
|
|
129
|
-
>
|
|
130
|
-
Return to Data Browser
|
|
131
|
-
</Link>
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (shouldShowModelNotFound) {
|
|
141
|
-
return (
|
|
142
|
-
<div className="flex flex-col justify-center py-12">
|
|
143
|
-
<div className="mt-8 mx-auto w-full max-w-md">
|
|
144
|
-
<div className="bg-white dark:bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
|
145
|
-
<div className="text-center">
|
|
146
|
-
<ExclamationCircleIcon className="mx-auto h-12 w-12 text-yellow-400" />
|
|
147
|
-
<h2 className="mt-4 text-lg font-medium text-gray-900 dark:text-gray-100">Model Not Found</h2>
|
|
148
|
-
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
149
|
-
The data model for "{validatedDataType}" could not be found.
|
|
150
|
-
</p>
|
|
151
|
-
<div className="mt-6">
|
|
152
|
-
<Link
|
|
153
|
-
to={basePath}
|
|
154
|
-
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-web hover:bg-green-web-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-web"
|
|
155
|
-
>
|
|
156
|
-
Return to Data Browser
|
|
157
|
-
</Link>
|
|
158
|
-
</div>
|
|
159
|
-
</div>
|
|
160
|
-
</div>
|
|
161
|
-
</div>
|
|
162
|
-
</div>
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// At this point we know model exists and is valid
|
|
167
|
-
return <AdminDataCreatePageContent model={model!} basePath={basePath} formTheme={formTheme} />
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// =================================
|
|
171
|
-
// CONTENT COMPONENT
|
|
172
|
-
// =================================
|
|
173
|
-
|
|
174
|
-
function AdminDataCreatePageContent({ model, basePath, formTheme }: Readonly<{ model: any; basePath: string; formTheme: any }>) {
|
|
175
|
-
const navigate = useNavigate()
|
|
176
|
-
const { sdk } = useAdminDataContext()
|
|
177
|
-
|
|
178
|
-
// State
|
|
179
|
-
const [submissionState, setSubmissionState] = useState<{
|
|
180
|
-
status: 'idle' | 'loading' | 'success' | 'error'
|
|
181
|
-
message?: string
|
|
182
|
-
}>({ status: 'idle' })
|
|
183
|
-
|
|
184
|
-
// Get GraphQL documents with error handling (memoized to prevent render loops)
|
|
185
|
-
const documents = useMemo(() => {
|
|
186
|
-
try {
|
|
187
|
-
return getAdminDocuments(sdk, model)
|
|
188
|
-
} catch (error) {
|
|
189
|
-
console.error('[AdminDataCreatePage] Error getting documents:', error)
|
|
190
|
-
return null
|
|
191
|
-
}
|
|
192
|
-
}, [sdk, model])
|
|
193
|
-
|
|
194
|
-
const CREATE_MUTATION = useMemo(() => {
|
|
195
|
-
if (!documents?.create) return null
|
|
196
|
-
try {
|
|
197
|
-
// Check if it's already a parsed GraphQL document
|
|
198
|
-
if (documents.create?.definitions && documents.create?.loc) {
|
|
199
|
-
return documents.create
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return gql(documents.create)
|
|
203
|
-
} catch (error) {
|
|
204
|
-
console.error('[AdminDataCreatePage] Error parsing CREATE mutation:', error)
|
|
205
|
-
return null
|
|
206
|
-
}
|
|
207
|
-
}, [documents])
|
|
208
|
-
|
|
209
|
-
// Create mutation - call BEFORE any early returns to maintain hook order
|
|
210
|
-
const [createMutation] = useMutation(
|
|
211
|
-
CREATE_MUTATION ||
|
|
212
|
-
gql`
|
|
213
|
-
mutation PlaceholderCreate {
|
|
214
|
-
__typename
|
|
215
|
-
}
|
|
216
|
-
`,
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
// Early return AFTER all hooks are called
|
|
220
|
-
if (!documents || !CREATE_MUTATION) {
|
|
221
|
-
return (
|
|
222
|
-
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
|
223
|
-
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-2xl">
|
|
224
|
-
<div className="bg-white dark:bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
|
225
|
-
<div className="text-center">
|
|
226
|
-
<ExclamationCircleIcon className="mx-auto h-12 w-12 text-red-400" />
|
|
227
|
-
<h2 className="mt-4 text-lg font-medium text-gray-900 dark:text-gray-100">GraphQL Schema Error</h2>
|
|
228
|
-
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
229
|
-
Unable to load GraphQL documents for this model. Please ensure the API server is
|
|
230
|
-
running and the GraphQL schema is up to date.
|
|
231
|
-
</p>
|
|
232
|
-
<div className="mt-6">
|
|
233
|
-
<Link
|
|
234
|
-
to={basePath}
|
|
235
|
-
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-web hover:bg-green-web-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-web"
|
|
236
|
-
>
|
|
237
|
-
Return to Data Browser
|
|
238
|
-
</Link>
|
|
239
|
-
</div>
|
|
240
|
-
</div>
|
|
241
|
-
</div>
|
|
242
|
-
</div>
|
|
243
|
-
</div>
|
|
244
|
-
)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Build form fields
|
|
248
|
-
const formFields = buildFormFields(sdk, model, 'create')
|
|
249
|
-
|
|
250
|
-
// Handle form submission
|
|
251
|
-
const handleSubmit = async (formData: Record<string, unknown>) => {
|
|
252
|
-
try {
|
|
253
|
-
setSubmissionState({ status: 'loading' })
|
|
254
|
-
|
|
255
|
-
// Clean the form input
|
|
256
|
-
const cleanedInput = cleanFormInput(formData, model)
|
|
257
|
-
|
|
258
|
-
// Execute mutation
|
|
259
|
-
const result = await createMutation({
|
|
260
|
-
variables: {
|
|
261
|
-
input: cleanedInput,
|
|
262
|
-
},
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
if ((result as any).errors) {
|
|
266
|
-
console.error('GraphQL errors:', (result as any).errors)
|
|
267
|
-
setSubmissionState({
|
|
268
|
-
status: 'error',
|
|
269
|
-
message: (result as any).errors.map((err: any) => err.message).join(', '),
|
|
270
|
-
})
|
|
271
|
-
return
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
setSubmissionState({
|
|
275
|
-
status: 'success',
|
|
276
|
-
message: `${toReadableText(model.name)} created successfully!`,
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
// Redirect after a brief delay to show success message
|
|
280
|
-
setTimeout(() => {
|
|
281
|
-
navigate(`/admin/data/${toKebabCase(model.pluralName)}`)
|
|
282
|
-
}, 1500)
|
|
283
|
-
} catch (error) {
|
|
284
|
-
console.error('Error creating record:', error)
|
|
285
|
-
setSubmissionState({
|
|
286
|
-
status: 'error',
|
|
287
|
-
message: error instanceof Error ? error.message : 'An unexpected error occurred',
|
|
288
|
-
})
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Clear submission state after errors
|
|
293
|
-
useEffect(() => {
|
|
294
|
-
if (submissionState.status === 'error') {
|
|
295
|
-
const timer = setTimeout(() => {
|
|
296
|
-
setSubmissionState({ status: 'idle' })
|
|
297
|
-
}, 5000)
|
|
298
|
-
return () => clearTimeout(timer)
|
|
299
|
-
}
|
|
300
|
-
}, [submissionState.status])
|
|
301
|
-
|
|
302
|
-
return (
|
|
303
|
-
<div className="space-y-6">
|
|
304
|
-
{/* Header */}
|
|
305
|
-
<div className="mb-8">
|
|
306
|
-
<nav className="flex mb-6" aria-label="Breadcrumb">
|
|
307
|
-
<ol className="flex items-center space-x-4">
|
|
308
|
-
<li>
|
|
309
|
-
<Link to="/admin/data" className="text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400">
|
|
310
|
-
Data Browser
|
|
311
|
-
</Link>
|
|
312
|
-
</li>
|
|
313
|
-
<li>
|
|
314
|
-
<div className="flex items-center">
|
|
315
|
-
<svg
|
|
316
|
-
className="flex-shrink-0 h-5 w-5 text-gray-300 dark:text-gray-600"
|
|
317
|
-
fill="currentColor"
|
|
318
|
-
viewBox="0 0 20 20"
|
|
319
|
-
>
|
|
320
|
-
<path
|
|
321
|
-
fillRule="evenodd"
|
|
322
|
-
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 111.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
323
|
-
clipRule="evenodd"
|
|
324
|
-
/>
|
|
325
|
-
</svg>
|
|
326
|
-
<Link
|
|
327
|
-
to={`/admin/data/${toKebabCase(model.pluralName)}`}
|
|
328
|
-
className="ml-4 text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400"
|
|
329
|
-
>
|
|
330
|
-
{toReadableText(model.pluralName)}
|
|
331
|
-
</Link>
|
|
332
|
-
</div>
|
|
333
|
-
</li>
|
|
334
|
-
<li>
|
|
335
|
-
<div className="flex items-center">
|
|
336
|
-
<svg
|
|
337
|
-
className="flex-shrink-0 h-5 w-5 text-gray-300 dark:text-gray-600"
|
|
338
|
-
fill="currentColor"
|
|
339
|
-
viewBox="0 0 20 20"
|
|
340
|
-
>
|
|
341
|
-
<path
|
|
342
|
-
fillRule="evenodd"
|
|
343
|
-
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 111.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
344
|
-
clipRule="evenodd"
|
|
345
|
-
/>
|
|
346
|
-
</svg>
|
|
347
|
-
<span className="ml-4 text-gray-500 dark:text-gray-400">Create New</span>
|
|
348
|
-
</div>
|
|
349
|
-
</li>
|
|
350
|
-
</ol>
|
|
351
|
-
</nav>
|
|
352
|
-
<h1 className="mt-4 text-3xl font-bold text-gray-900 dark:text-gray-100">
|
|
353
|
-
Create {toReadableText(model.name)}
|
|
354
|
-
</h1>
|
|
355
|
-
</div>
|
|
356
|
-
|
|
357
|
-
{/* Submission Status */}
|
|
358
|
-
{submissionState.status !== 'idle' && (
|
|
359
|
-
<div
|
|
360
|
-
className={`mb-6 rounded-md p-4 ${
|
|
361
|
-
submissionState.status === 'success'
|
|
362
|
-
? 'bg-green-50 border border-green-200'
|
|
363
|
-
: submissionState.status === 'error'
|
|
364
|
-
? 'bg-red-50 border border-red-200'
|
|
365
|
-
: 'bg-blue-50 border border-blue-200'
|
|
366
|
-
}`}
|
|
367
|
-
>
|
|
368
|
-
<div className="flex">
|
|
369
|
-
<div className="flex-shrink-0">
|
|
370
|
-
{submissionState.status === 'success' ? (
|
|
371
|
-
<CheckCircleIcon className="h-5 w-5 text-green-400" />
|
|
372
|
-
) : submissionState.status === 'error' ? (
|
|
373
|
-
<ExclamationCircleIcon className="h-5 w-5 text-red-400" />
|
|
374
|
-
) : (
|
|
375
|
-
<div className="h-5 w-5 border-2 border-blue-400 border-t-transparent rounded-full animate-spin" />
|
|
376
|
-
)}
|
|
377
|
-
</div>
|
|
378
|
-
<div className="ml-3">
|
|
379
|
-
<p
|
|
380
|
-
className={`text-sm font-medium ${
|
|
381
|
-
submissionState.status === 'success'
|
|
382
|
-
? 'text-green-800'
|
|
383
|
-
: submissionState.status === 'error'
|
|
384
|
-
? 'text-red-800'
|
|
385
|
-
: 'text-blue-800'
|
|
386
|
-
}`}
|
|
387
|
-
>
|
|
388
|
-
{submissionState.status === 'loading'
|
|
389
|
-
? `Creating ${toReadableText(model.name)}...`
|
|
390
|
-
: submissionState.message}
|
|
391
|
-
</p>
|
|
392
|
-
</div>
|
|
393
|
-
</div>
|
|
394
|
-
</div>
|
|
395
|
-
)}
|
|
396
|
-
|
|
397
|
-
{/* Form */}
|
|
398
|
-
<div className="bg-white dark:bg-gray-800 shadow-sm rounded-lg">
|
|
399
|
-
<div className="px-6 py-8">
|
|
400
|
-
<Form
|
|
401
|
-
id={`create-${model.name.toLowerCase()}-form`}
|
|
402
|
-
fields={formFields}
|
|
403
|
-
submit={handleSubmit}
|
|
404
|
-
disabled={submissionState.status === 'loading'}
|
|
405
|
-
theme={formTheme}
|
|
406
|
-
/>
|
|
407
|
-
</div>
|
|
408
|
-
</div>
|
|
409
|
-
</div>
|
|
410
|
-
)
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
export function AdminDataCreateErrorBoundary({ error }: Readonly<{ error: Error }>) {
|
|
414
|
-
return <ErrorBoundary error={error} />
|
|
415
|
-
}
|