@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.
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 +16 -3
  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
package/project.json DELETED
@@ -1,35 +0,0 @@
1
- {
2
- "name": "admin-data",
3
- "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
- "sourceRoot": "libs/admin-data/src",
5
- "projectType": "library",
6
- "tags": ["publishable"],
7
- "targets": {
8
- "build": {
9
- "executor": "nx:run-commands",
10
- "outputs": ["{workspaceRoot}/dist/libs/admin-data"],
11
- "options": {
12
- "commands": [
13
- "vite build",
14
- "cp README.md ../../dist/libs/admin-data/",
15
- "cp package.json ../../dist/libs/admin-data/"
16
- ],
17
- "cwd": "libs/admin-data",
18
- "parallel": false
19
- }
20
- },
21
- "publish": {
22
- "executor": "nx:run-commands",
23
- "dependsOn": ["build"],
24
- "options": {
25
- "command": "npm publish dist/libs/admin-data --access public"
26
- }
27
- },
28
- "version": {
29
- "executor": "@jscutlery/semver:version",
30
- "options": {
31
- "preset": "conventional"
32
- }
33
- }
34
- }
35
- }
package/src/index.ts DELETED
@@ -1,28 +0,0 @@
1
- // Context
2
- export * from './lib/context/AdminDataContext'
3
-
4
- // Main components
5
- export * from './lib/pages/AdminDataCreatePage'
6
- export * from './lib/pages/AdminDataEditPage'
7
- export * from './lib/pages/AdminDataListPage'
8
- export * from './lib/pages/AdminDataIndexPage'
9
-
10
- // Layouts
11
- export * from './lib/layouts/AdminDataLayout'
12
-
13
- // Types
14
- export * from './lib/types'
15
-
16
- // Hooks
17
- export * from './lib/hooks/useAdminList'
18
- export * from './lib/hooks/useClickOutside'
19
- export * from './lib/hooks/useDebounce'
20
- export * from './lib/hooks/useRelationData'
21
-
22
- // Components
23
- export * from './lib/components'
24
-
25
- // Utilities
26
- export * from './lib/utils/graphql-utils'
27
- export * from './lib/utils/secure-storage'
28
- export * from './lib/utils/string-utils'
@@ -1,10 +0,0 @@
1
- import { render } from '@testing-library/react'
2
-
3
- import AdminData from './admin-data'
4
-
5
- describe('AdminData', () => {
6
- it('should render successfully', () => {
7
- const { baseElement } = render(<AdminData />)
8
- expect(baseElement).toBeTruthy()
9
- })
10
- })
@@ -1,9 +0,0 @@
1
- export function AdminData() {
2
- return (
3
- <div>
4
- <h1>Welcome to AdminData!</h1>
5
- </div>
6
- )
7
- }
8
-
9
- export default AdminData
@@ -1,88 +0,0 @@
1
- import { formatFieldName } from '../../utils/string-utils'
2
-
3
- interface DateRangeFilterProps {
4
- fieldName: string
5
- currentValue: any
6
- onChange: (value: any) => void
7
- }
8
-
9
- // Component for date range filtering
10
- export function DateRangeFilter({
11
- fieldName,
12
- currentValue,
13
- onChange
14
- }: DateRangeFilterProps) {
15
- // Parse current value if it's a range object
16
- const fromDate = currentValue?.gte ? new Date(currentValue.gte).toISOString().split('T')[0] : ''
17
- const toDate = currentValue?.lte ? new Date(currentValue.lte).toISOString().split('T')[0] : ''
18
-
19
- const handleFromChange = (date: string) => {
20
- const newValue = { ...currentValue }
21
- if (date) {
22
- newValue.gte = new Date(date).toISOString()
23
- } else {
24
- delete newValue.gte
25
- }
26
-
27
- // If no from or to date, clear the filter entirely
28
- if (!newValue.gte && !newValue.lte) {
29
- onChange(undefined)
30
- } else {
31
- onChange(newValue)
32
- }
33
- }
34
-
35
- const handleToChange = (date: string) => {
36
- const newValue = { ...currentValue }
37
- if (date) {
38
- // Set to end of day for "to" date
39
- const endOfDay = new Date(date)
40
- endOfDay.setHours(23, 59, 59, 999)
41
- newValue.lte = endOfDay.toISOString()
42
- } else {
43
- delete newValue.lte
44
- }
45
-
46
- // If no from or to date, clear the filter entirely
47
- if (!newValue.gte && !newValue.lte) {
48
- onChange(undefined)
49
- } else {
50
- onChange(newValue)
51
- }
52
- }
53
-
54
- return (
55
- <div className="space-y-1">
56
- <label className="block text-sm font-medium text-gray-700">
57
- {formatFieldName(fieldName)}
58
- </label>
59
- <div className="grid grid-cols-2 gap-2">
60
- <div>
61
- <label className="block text-xs text-gray-500 mb-1">From</label>
62
- <input
63
- type="date"
64
- value={fromDate}
65
- onChange={(e) => handleFromChange(e.target.value)}
66
- className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-green-web focus:border-green-web"
67
- />
68
- </div>
69
- <div>
70
- <label className="block text-xs text-gray-500 mb-1">To</label>
71
- <input
72
- type="date"
73
- value={toDate}
74
- onChange={(e) => handleToChange(e.target.value)}
75
- className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-green-web focus:border-green-web"
76
- />
77
- </div>
78
- </div>
79
- {(fromDate || toDate) && (
80
- <div className="text-xs text-gray-500">
81
- {fromDate && toDate && `${fromDate} to ${toDate}`}
82
- {fromDate && !toDate && `From ${fromDate}`}
83
- {!fromDate && toDate && `Until ${toDate}`}
84
- </div>
85
- )}
86
- </div>
87
- )
88
- }
@@ -1,111 +0,0 @@
1
- import { formatFieldName } from '../../utils/string-utils'
2
-
3
- interface NumberRangeFilterProps {
4
- fieldName: string
5
- fieldType: string
6
- currentValue: any
7
- onChange: (value: any) => void
8
- }
9
-
10
- export function NumberRangeFilter({
11
- fieldName,
12
- fieldType,
13
- currentValue,
14
- onChange
15
- }: Readonly<NumberRangeFilterProps>) {
16
- // Parse current value if it's a range object
17
- const minValue = currentValue?.gte !== undefined ? currentValue.gte.toString() : ''
18
- const maxValue = currentValue?.lte !== undefined ? currentValue.lte.toString() : ''
19
-
20
- const parseNumber = (value: string) => {
21
- if (!value) return undefined
22
-
23
- // Parse based on field type
24
- if (fieldType === 'int' || fieldType === 'bigint') {
25
- const parsed = parseInt(value, 10)
26
- return isNaN(parsed) ? undefined : parsed
27
- } else if (fieldType === 'float' || fieldType === 'decimal') {
28
- const parsed = parseFloat(value)
29
- return isNaN(parsed) ? undefined : parsed
30
- }
31
- return undefined
32
- }
33
-
34
- const handleMinChange = (value: string) => {
35
- const newValue = { ...currentValue }
36
- const parsedValue = parseNumber(value)
37
-
38
- if (parsedValue !== undefined) {
39
- newValue.gte = parsedValue
40
- } else {
41
- delete newValue.gte
42
- }
43
-
44
- // If no min or max value, clear the filter entirely
45
- if (newValue.gte === undefined && newValue.lte === undefined) {
46
- onChange(undefined)
47
- } else {
48
- onChange(newValue)
49
- }
50
- }
51
-
52
- const handleMaxChange = (value: string) => {
53
- const newValue = { ...currentValue }
54
- const parsedValue = parseNumber(value)
55
-
56
- if (parsedValue !== undefined) {
57
- newValue.lte = parsedValue
58
- } else {
59
- delete newValue.lte
60
- }
61
-
62
- // If no min or max value, clear the filter entirely
63
- if (newValue.gte === undefined && newValue.lte === undefined) {
64
- onChange(undefined)
65
- } else {
66
- onChange(newValue)
67
- }
68
- }
69
-
70
- // Determine input step based on field type
71
- const step = fieldType === 'int' || fieldType === 'bigint' ? '1' : 'any'
72
-
73
- return (
74
- <div className="space-y-1">
75
- <label className="block text-sm font-medium text-gray-700">
76
- {formatFieldName(fieldName)}
77
- </label>
78
- <div className="grid grid-cols-2 gap-2">
79
- <div>
80
- <label className="block text-xs text-gray-500 mb-1">Min</label>
81
- <input
82
- type="number"
83
- step={step}
84
- value={minValue}
85
- onChange={(e) => handleMinChange(e.target.value)}
86
- placeholder="No minimum"
87
- className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-green-web focus:border-green-web"
88
- />
89
- </div>
90
- <div>
91
- <label className="block text-xs text-gray-500 mb-1">Max</label>
92
- <input
93
- type="number"
94
- step={step}
95
- value={maxValue}
96
- onChange={(e) => handleMaxChange(e.target.value)}
97
- placeholder="No maximum"
98
- className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-green-web focus:border-green-web"
99
- />
100
- </div>
101
- </div>
102
- {(minValue || maxValue) && (
103
- <div className="text-xs text-gray-500">
104
- {minValue && maxValue && `${minValue} to ${maxValue}`}
105
- {minValue && !maxValue && `≥ ${minValue}`}
106
- {!minValue && maxValue && `≤ ${maxValue}`}
107
- </div>
108
- )}
109
- </div>
110
- )
111
- }
@@ -1,176 +0,0 @@
1
- import { getItemDisplayName } from '../../utils/string-utils'
2
-
3
- // Dropdown button component
4
- export function RelationDropdownButton({
5
- currentItem,
6
- relatedModelName,
7
- isOpen,
8
- onClick
9
- }: Readonly<{
10
- currentItem: any
11
- relatedModelName: string
12
- isOpen: boolean
13
- onClick: () => void
14
- }>) {
15
- return (
16
- <button
17
- type="button"
18
- onClick={onClick}
19
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-web focus:border-green-web text-sm text-left bg-white flex items-center justify-between"
20
- >
21
- <span className={currentItem ? 'text-gray-900' : 'text-gray-500'}>
22
- {currentItem ? getItemDisplayName(currentItem) : `Select ${relatedModelName.toLowerCase()}...`}
23
- </span>
24
- <svg
25
- className={`w-4 h-4 text-gray-400 transition-transform ${isOpen ? 'rotate-180' : ''}`}
26
- fill="none"
27
- stroke="currentColor"
28
- viewBox="0 0 24 24"
29
- >
30
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
31
- </svg>
32
- </button>
33
- )
34
- }
35
-
36
- // Search input component
37
- export function RelationSearchInput({
38
- searchTerm,
39
- onSearchChange,
40
- relatedModelName
41
- }: Readonly<{
42
- searchTerm: string
43
- onSearchChange: (value: string) => void
44
- relatedModelName: string
45
- }>) {
46
- return (
47
- <div className="p-2">
48
- <input
49
- type="text"
50
- placeholder={`Search ${relatedModelName.toLowerCase()}...`}
51
- value={searchTerm}
52
- onChange={(e) => onSearchChange(e.target.value)}
53
- className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-green-web"
54
- autoFocus
55
- />
56
- </div>
57
- )
58
- }
59
-
60
- // Individual item component
61
- export function RelationItem({
62
- item,
63
- onSelect
64
- }: {
65
- item: any
66
- onSelect: (item: any) => void
67
- }) {
68
- return (
69
- <button
70
- type="button"
71
- onClick={() => onSelect(item)}
72
- className="w-full px-3 py-2 text-left text-sm hover:bg-gray-100 focus:bg-gray-100 transition-colors"
73
- >
74
- {getItemDisplayName(item)}
75
- </button>
76
- )
77
- }
78
-
79
- // Item list component with error handling
80
- export function RelationItemList({
81
- items,
82
- loading,
83
- error,
84
- onSelect,
85
- onClear
86
- }: Readonly<{
87
- items: any[]
88
- loading: boolean
89
- error?: any
90
- onSelect: (item: any) => void
91
- onClear: () => void
92
- }>) {
93
- return (
94
- <div className="max-h-48 overflow-y-auto">
95
- <button
96
- type="button"
97
- onClick={onClear}
98
- className="w-full px-3 py-2 text-left text-sm hover:bg-gray-100 text-gray-500 border-b border-gray-200"
99
- >
100
- Clear selection
101
- </button>
102
-
103
- {error ? (
104
- <div className="px-3 py-2 text-sm text-red-600">
105
- <div className="flex items-center">
106
- <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
107
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
108
- </svg>
109
- Failed to load options
110
- </div>
111
- <div className="text-xs text-gray-500 mt-1">
112
- {error.networkError ? 'Network error' : 'Please try again'}
113
- </div>
114
- </div>
115
- ) : loading ? (
116
- <div className="px-3 py-2 text-sm text-gray-500">
117
- <div className="flex items-center">
118
- <svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
119
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
120
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
121
- </svg>
122
- Loading...
123
- </div>
124
- </div>
125
- ) : items.length === 0 ? (
126
- <div className="px-3 py-2 text-sm text-gray-500">No items found</div>
127
- ) : (
128
- items.map((item: any) => (
129
- <RelationItem
130
- key={item.id}
131
- item={item}
132
- onSelect={onSelect}
133
- />
134
- ))
135
- )}
136
- </div>
137
- )
138
- }
139
-
140
- // Dropdown content container with error handling
141
- export function RelationDropdownContent({
142
- searchTerm,
143
- onSearchChange,
144
- relatedModelName,
145
- items,
146
- loading,
147
- error,
148
- onSelect,
149
- onClear
150
- }: {
151
- searchTerm: string
152
- onSearchChange: (value: string) => void
153
- relatedModelName: string
154
- items: any[]
155
- loading: boolean
156
- error?: any
157
- onSelect: (item: any) => void
158
- onClear: () => void
159
- }) {
160
- return (
161
- <div className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg">
162
- <RelationSearchInput
163
- searchTerm={searchTerm}
164
- onSearchChange={onSearchChange}
165
- relatedModelName={relatedModelName}
166
- />
167
- <RelationItemList
168
- items={items}
169
- loading={loading}
170
- error={error}
171
- onSelect={onSelect}
172
- onClear={onClear}
173
- />
174
- </div>
175
- )
176
- }
@@ -1,106 +0,0 @@
1
- import { useCallback, useMemo, useRef, useState } from 'react'
2
- import { useClickOutside } from '../../hooks/useClickOutside'
3
- import { useRelationData } from '../../hooks/useRelationData'
4
- import { formatFieldName } from '../../utils/string-utils'
5
- import { RelationDropdownButton, RelationDropdownContent } from './RelationComponents'
6
-
7
- interface RelationFilterFieldProps {
8
- fieldName: string
9
- relatedModelName: string
10
- currentValue: any
11
- onChange: (value: any) => void
12
- }
13
-
14
- // Refactored relation field filtering component
15
- export function RelationFilterField({
16
- fieldName,
17
- relatedModelName,
18
- currentValue,
19
- onChange
20
- }: Readonly<RelationFilterFieldProps>) {
21
- const [searchTerm, setSearchTerm] = useState('')
22
- const [isOpen, setIsOpen] = useState(false)
23
- const dropdownRef = useRef<HTMLDivElement>(null)
24
-
25
- // Memoize the close handler to prevent memory leaks
26
- const handleCloseDropdown = useCallback(() => {
27
- setIsOpen(false)
28
- }, [])
29
-
30
- // Custom hooks for behavior
31
- useClickOutside(dropdownRef, handleCloseDropdown, isOpen)
32
- const { relatedItems, loading, error: relationError, hasDocument } = useRelationData(relatedModelName, searchTerm, isOpen)
33
-
34
- // Find current item to display
35
- const currentItem = useMemo(() =>
36
- currentValue?.id ? relatedItems.find((item: any) => item.id === currentValue.id) : null,
37
- [currentValue?.id, relatedItems]
38
- )
39
-
40
- // Event handlers
41
- const handleSelect = useCallback((item: any) => {
42
- onChange({ id: item.id })
43
- setIsOpen(false)
44
- setSearchTerm('') // Reset search when item selected
45
- }, [onChange])
46
-
47
- const handleClear = useCallback(() => {
48
- onChange(undefined)
49
- setIsOpen(false)
50
- setSearchTerm('')
51
- }, [onChange])
52
-
53
- const handleToggleOpen = useCallback(() => {
54
- setIsOpen(!isOpen)
55
- if (!isOpen) {
56
- setSearchTerm('') // Reset search when opening
57
- }
58
- }, [isOpen])
59
-
60
- // If no document available, fall back to text input
61
- if (!hasDocument) {
62
- return (
63
- <div className="space-y-1">
64
- <label className="block text-sm font-medium text-gray-700">
65
- {formatFieldName(fieldName)} ID
66
- </label>
67
- <input
68
- type="text"
69
- value={currentValue?.id || ''}
70
- onChange={(e) => onChange(e.target.value ? { id: e.target.value } : undefined)}
71
- placeholder="Enter ID..."
72
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-web focus:border-green-web text-sm"
73
- />
74
- </div>
75
- )
76
- }
77
-
78
- return (
79
- <div className="space-y-1" ref={dropdownRef}>
80
- <label className="block text-sm font-medium text-gray-700">
81
- {formatFieldName(fieldName)}
82
- </label>
83
- <div className="relative">
84
- <RelationDropdownButton
85
- currentItem={currentItem}
86
- relatedModelName={relatedModelName}
87
- isOpen={isOpen}
88
- onClick={handleToggleOpen}
89
- />
90
-
91
- {isOpen && (
92
- <RelationDropdownContent
93
- searchTerm={searchTerm}
94
- onSearchChange={setSearchTerm}
95
- relatedModelName={relatedModelName}
96
- items={relatedItems}
97
- loading={loading}
98
- error={relationError}
99
- onSelect={handleSelect}
100
- onClear={handleClear}
101
- />
102
- )}
103
- </div>
104
- </div>
105
- )
106
- }
@@ -1,5 +0,0 @@
1
- export { DateRangeFilter } from './DateRangeFilter'
2
- export { NumberRangeFilter } from './NumberRangeFilter'
3
- export * from './RelationComponents'
4
- export { RelationFilterField } from './RelationFilterField'
5
-
@@ -1,2 +0,0 @@
1
- export * from './filters'
2
- export * from './shared'
@@ -1,88 +0,0 @@
1
- import React from 'react'
2
- import { Link } from 'react-router'
3
- import { ChevronRightIcon, HomeIcon } from '@heroicons/react/24/outline'
4
-
5
- export interface BreadcrumbItem {
6
- id: string
7
- label: string
8
- href?: string
9
- isActive?: boolean
10
- }
11
-
12
- export interface AdminBreadcrumbsProps {
13
- readonly items: readonly BreadcrumbItem[]
14
- readonly showHome?: boolean
15
- readonly homeHref?: string
16
- readonly className?: string
17
- }
18
-
19
- export function AdminBreadcrumbs({
20
- items,
21
- showHome = true,
22
- homeHref = '/admin',
23
- className = '',
24
- }: Readonly<AdminBreadcrumbsProps>) {
25
- // Extract nested ternary logic into clear helper functions
26
- const renderHomeLink = () => {
27
- if (!showHome) return null
28
-
29
- return (
30
- <li key="home" className="flex items-center">
31
- <Link
32
- to={homeHref}
33
- className="text-gray-400 hover:text-gray-500"
34
- aria-label="Home"
35
- >
36
- <HomeIcon className="h-5 w-5 flex-shrink-0" />
37
- </Link>
38
- </li>
39
- )
40
- }
41
-
42
- const renderBreadcrumbItem = (item: BreadcrumbItem, index: number, isLast: boolean) => {
43
- const content = item.href && !item.isActive ? (
44
- <Link
45
- to={item.href}
46
- className="text-sm font-medium text-gray-500 hover:text-gray-700"
47
- >
48
- {item.label}
49
- </Link>
50
- ) : (
51
- <span
52
- className={`text-sm font-medium ${
53
- item.isActive ? 'text-gray-900' : 'text-gray-500'
54
- }`}
55
- >
56
- {item.label}
57
- </span>
58
- )
59
-
60
- return (
61
- <li key={item.id} className="flex items-center">
62
- {(showHome || index > 0) && (
63
- <ChevronRightIcon
64
- className="h-5 w-5 flex-shrink-0 text-gray-400 mr-4"
65
- aria-hidden="true"
66
- />
67
- )}
68
- {content}
69
- </li>
70
- )
71
- }
72
-
73
- const shouldShowSeparator = (index: number) => {
74
- return showHome || index > 0
75
- }
76
-
77
- return (
78
- <nav className={`flex ${className}`} aria-label="Breadcrumb">
79
- <ol className="flex items-center space-x-4">
80
- {renderHomeLink()}
81
- {items.map((item, index) => {
82
- const isLast = index === items.length - 1
83
- return renderBreadcrumbItem(item, index, isLast)
84
- })}
85
- </ol>
86
- </nav>
87
- )
88
- }