@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
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'
|
package/src/lib/admin-data.tsx
DELETED
|
@@ -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,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
|
-
}
|