@nestledjs/data-browser 0.1.2 → 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.
Files changed (83) hide show
  1. package/index.d.ts +15 -0
  2. package/index.js +64 -0
  3. package/lib/admin-data.d.ts +2 -0
  4. package/lib/components/filters/DateRangeFilter.d.ts +7 -0
  5. package/lib/components/filters/DateRangeFilter.js +105 -0
  6. package/lib/components/filters/NumberRangeFilter.d.ts +8 -0
  7. package/lib/components/filters/NumberRangeFilter.js +115 -0
  8. package/lib/components/filters/RelationComponents.d.ts +32 -0
  9. package/lib/components/filters/RelationComponents.js +265 -0
  10. package/lib/components/filters/RelationFilterField.d.ts +8 -0
  11. package/lib/components/filters/RelationFilterField.js +116 -0
  12. package/lib/components/filters/index.d.ts +4 -0
  13. package/lib/components/index.d.ts +2 -0
  14. package/lib/components/shared/AdminBreadcrumbs.d.ts +13 -0
  15. package/lib/components/shared/AdminBreadcrumbs.js +103 -0
  16. package/lib/components/shared/AdminErrorStates.d.ts +30 -0
  17. package/lib/components/shared/AdminErrorStates.js +214 -0
  18. package/lib/components/shared/AdminStatusDisplay.d.ts +21 -0
  19. package/lib/components/shared/AdminStatusDisplay.js +205 -0
  20. package/lib/components/shared/index.d.ts +3 -0
  21. package/lib/context/AdminDataContext.d.ts +47 -0
  22. package/lib/context/AdminDataContext.js +28 -0
  23. package/lib/hooks/useAdminList.d.ts +5 -0
  24. package/lib/hooks/useAdminList.js +41 -0
  25. package/lib/hooks/useClickOutside.d.ts +2 -0
  26. package/lib/hooks/useClickOutside.js +13 -0
  27. package/lib/hooks/useDebounce.d.ts +1 -0
  28. package/lib/hooks/useDebounce.js +13 -0
  29. package/lib/hooks/useRelationData.d.ts +7 -0
  30. package/lib/hooks/useRelationData.js +64 -0
  31. package/lib/layouts/AdminDataLayout.d.ts +5 -0
  32. package/lib/layouts/AdminDataLayout.js +315 -0
  33. package/lib/pages/AdminDataCreatePage.d.ts +4 -0
  34. package/lib/pages/AdminDataCreatePage.js +487 -0
  35. package/lib/pages/AdminDataEditPage.d.ts +4 -0
  36. package/lib/pages/AdminDataEditPage.js +903 -0
  37. package/lib/pages/AdminDataIndexPage.d.ts +4 -0
  38. package/lib/pages/AdminDataIndexPage.js +91 -0
  39. package/lib/pages/AdminDataListPage.d.ts +9 -0
  40. package/lib/pages/AdminDataListPage.js +1218 -0
  41. package/lib/types/index.d.ts +60 -0
  42. package/lib/types/index.js +16 -0
  43. package/lib/utils/graphql-utils.d.ts +25 -0
  44. package/lib/utils/graphql-utils.js +265 -0
  45. package/lib/utils/secure-storage.d.ts +50 -0
  46. package/lib/utils/secure-storage.js +145 -0
  47. package/lib/utils/string-utils.d.ts +6 -0
  48. package/lib/utils/string-utils.js +27 -0
  49. package/package.json +8 -2
  50. package/.babelrc +0 -12
  51. package/LICENSE +0 -21
  52. package/eslint.config.mjs +0 -12
  53. package/project.json +0 -35
  54. package/src/index.ts +0 -28
  55. package/src/lib/admin-data.spec.tsx +0 -10
  56. package/src/lib/admin-data.tsx +0 -9
  57. package/src/lib/components/filters/DateRangeFilter.tsx +0 -88
  58. package/src/lib/components/filters/NumberRangeFilter.tsx +0 -111
  59. package/src/lib/components/filters/RelationComponents.tsx +0 -176
  60. package/src/lib/components/filters/RelationFilterField.tsx +0 -106
  61. package/src/lib/components/filters/index.ts +0 -5
  62. package/src/lib/components/index.ts +0 -2
  63. package/src/lib/components/shared/AdminBreadcrumbs.tsx +0 -88
  64. package/src/lib/components/shared/AdminErrorStates.tsx +0 -180
  65. package/src/lib/components/shared/AdminStatusDisplay.tsx +0 -292
  66. package/src/lib/components/shared/index.ts +0 -3
  67. package/src/lib/context/AdminDataContext.tsx +0 -74
  68. package/src/lib/hooks/useAdminList.ts +0 -42
  69. package/src/lib/hooks/useClickOutside.ts +0 -21
  70. package/src/lib/hooks/useDebounce.ts +0 -16
  71. package/src/lib/hooks/useRelationData.ts +0 -114
  72. package/src/lib/layouts/AdminDataLayout.tsx +0 -251
  73. package/src/lib/pages/AdminDataCreatePage.tsx +0 -415
  74. package/src/lib/pages/AdminDataEditPage.tsx +0 -777
  75. package/src/lib/pages/AdminDataIndexPage.tsx +0 -50
  76. package/src/lib/pages/AdminDataListPage.tsx +0 -921
  77. package/src/lib/types/index.ts +0 -51
  78. package/src/lib/utils/graphql-utils.ts +0 -538
  79. package/src/lib/utils/secure-storage.ts +0 -305
  80. package/src/lib/utils/string-utils.ts +0 -53
  81. package/tsconfig.json +0 -17
  82. package/tsconfig.lib.json +0 -20
  83. 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
- }