@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.
- 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 +8 -2
- 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,114 +0,0 @@
|
|
|
1
|
-
import { useQuery } from '@apollo/client/react'
|
|
2
|
-
import { useMemo } from 'react'
|
|
3
|
-
import { useAdminDataContext } from '../context/AdminDataContext'
|
|
4
|
-
import { getAdminDocuments } from '../utils/graphql-utils'
|
|
5
|
-
import { getSmartSearchFields } from '../utils/string-utils'
|
|
6
|
-
import { useDebounce } from './useDebounce'
|
|
7
|
-
|
|
8
|
-
// Custom hook for relation data fetching and management
|
|
9
|
-
export function useRelationData(relatedModelName: string, searchTerm: string, isOpen: boolean) {
|
|
10
|
-
const { sdk, databaseModels } = useAdminDataContext()
|
|
11
|
-
const debouncedSearchTerm = useDebounce(searchTerm, 300)
|
|
12
|
-
|
|
13
|
-
// Get the related model and its GraphQL documents
|
|
14
|
-
const relatedModel = useMemo(
|
|
15
|
-
() => databaseModels.find((m: any) => m.name === relatedModelName),
|
|
16
|
-
[databaseModels, relatedModelName],
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
const relatedDocuments = useMemo(
|
|
20
|
-
() => (relatedModel ? getAdminDocuments(sdk, relatedModel) : { listQuery: undefined }),
|
|
21
|
-
[sdk, relatedModel],
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
const relatedDataPath = useMemo(
|
|
25
|
-
() =>
|
|
26
|
-
relatedModel?.pluralModelPropertyName ||
|
|
27
|
-
relatedModelName.charAt(0).toLowerCase() + relatedModelName.slice(1) + 's',
|
|
28
|
-
[relatedModel, relatedModelName],
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
// Get searchable fields for the related model
|
|
32
|
-
const searchableFields = useMemo(() => {
|
|
33
|
-
if (!relatedModel) return []
|
|
34
|
-
|
|
35
|
-
return relatedModel.fields
|
|
36
|
-
.filter((f: any) => {
|
|
37
|
-
const fieldType = f.type.toLowerCase()
|
|
38
|
-
return (
|
|
39
|
-
!f.relationName &&
|
|
40
|
-
(fieldType === 'string' || fieldType.includes('text') || fieldType === 'boolean')
|
|
41
|
-
)
|
|
42
|
-
})
|
|
43
|
-
.map((f: any) => f.name)
|
|
44
|
-
}, [relatedModel])
|
|
45
|
-
|
|
46
|
-
const searchFields = useMemo(() => getSmartSearchFields(searchableFields), [searchableFields])
|
|
47
|
-
|
|
48
|
-
// Query variables for relation search
|
|
49
|
-
const queryVariables = useMemo(
|
|
50
|
-
() => ({
|
|
51
|
-
input: {
|
|
52
|
-
take: 20,
|
|
53
|
-
...(debouncedSearchTerm.trim()
|
|
54
|
-
? {
|
|
55
|
-
search: debouncedSearchTerm,
|
|
56
|
-
searchFields: searchFields.length > 0 ? searchFields : undefined,
|
|
57
|
-
}
|
|
58
|
-
: {}),
|
|
59
|
-
},
|
|
60
|
-
}),
|
|
61
|
-
[debouncedSearchTerm, searchFields],
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
// Query for related items with comprehensive error handling
|
|
65
|
-
const {
|
|
66
|
-
data: relatedData,
|
|
67
|
-
loading,
|
|
68
|
-
error: relationError,
|
|
69
|
-
} = useQuery(relatedDocuments.listQuery, {
|
|
70
|
-
variables: queryVariables,
|
|
71
|
-
skip: !relatedDocuments.listQuery || !isOpen,
|
|
72
|
-
errorPolicy: 'all', // Continue processing even if there are GraphQL errors
|
|
73
|
-
notifyOnNetworkStatusChange: true,
|
|
74
|
-
fetchPolicy: 'cache-first', // Use cache for better performance
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
// Validate and sanitize relation data
|
|
78
|
-
const relatedItems = useMemo(() => {
|
|
79
|
-
if (relationError) {
|
|
80
|
-
console.warn('[RelationData] GraphQL error:', relationError.message)
|
|
81
|
-
return []
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (!relatedData) return []
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
const items = (relatedData as any)[relatedDataPath] || []
|
|
88
|
-
|
|
89
|
-
// Validate that items is an array
|
|
90
|
-
if (!Array.isArray(items)) {
|
|
91
|
-
console.warn('[RelationData] Expected array but got:', typeof items)
|
|
92
|
-
return []
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Validate each item has required properties
|
|
96
|
-
return items.filter(item => {
|
|
97
|
-
if (!item || typeof item !== 'object') return false
|
|
98
|
-
if (!item.id) return false // Require ID field
|
|
99
|
-
return true
|
|
100
|
-
})
|
|
101
|
-
} catch (error) {
|
|
102
|
-
console.error('[RelationData] Error processing relation data:', error)
|
|
103
|
-
return []
|
|
104
|
-
}
|
|
105
|
-
}, [relatedData, relatedDataPath, relationError])
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
relatedModel,
|
|
109
|
-
relatedItems,
|
|
110
|
-
loading,
|
|
111
|
-
error: relationError,
|
|
112
|
-
hasDocument: !!relatedDocuments.listQuery,
|
|
113
|
-
}
|
|
114
|
-
}
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import { getPluralName } from '@nestledjs/helpers'
|
|
2
|
-
import { Outlet, useNavigate, useParams } from 'react-router'
|
|
3
|
-
import { useMemo, useState, useRef } from 'react'
|
|
4
|
-
import { AdminLocalStorage } from '../utils/secure-storage'
|
|
5
|
-
import { kebabCase, spacedWords } from '../utils/string-utils'
|
|
6
|
-
import { useAdminDataContext } from '../context/AdminDataContext'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Main layout component for admin data browser
|
|
10
|
-
* Provides model selector, fullscreen toggle, and preferences import/export
|
|
11
|
-
*/
|
|
12
|
-
export function AdminDataLayout() {
|
|
13
|
-
const navigate = useNavigate()
|
|
14
|
-
const params = useParams()
|
|
15
|
-
const { databaseModels, basePath = '/admin/data' } = useAdminDataContext()
|
|
16
|
-
|
|
17
|
-
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
18
|
-
const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null)
|
|
19
|
-
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
20
|
-
|
|
21
|
-
// Sort models alphabetically by plural display name
|
|
22
|
-
const sortedModels = useMemo(
|
|
23
|
-
() =>
|
|
24
|
-
[...databaseModels].sort((a, b) =>
|
|
25
|
-
getPluralName(spacedWords(a.name)).localeCompare(getPluralName(spacedWords(b.name))),
|
|
26
|
-
),
|
|
27
|
-
[databaseModels],
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
// Find the current model based on route params
|
|
31
|
-
const currentModel = useMemo(() => {
|
|
32
|
-
const pluralParam = params.dataTypePlural
|
|
33
|
-
if (!pluralParam) return null
|
|
34
|
-
|
|
35
|
-
return sortedModels.find(m => {
|
|
36
|
-
const modelUrlName = kebabCase(getPluralName(m.name))
|
|
37
|
-
return modelUrlName.toLowerCase() === pluralParam.toLowerCase()
|
|
38
|
-
})
|
|
39
|
-
}, [params.dataTypePlural, sortedModels])
|
|
40
|
-
|
|
41
|
-
// Handle model selection
|
|
42
|
-
const handleModelChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
43
|
-
const selectedModelName = event.target.value
|
|
44
|
-
if (selectedModelName) {
|
|
45
|
-
const selectedModel = sortedModels.find(m => m.name === selectedModelName)
|
|
46
|
-
if (selectedModel) {
|
|
47
|
-
navigate(`${basePath}/${kebabCase(getPluralName(selectedModel.name))}`)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const toggleFullscreen = () => {
|
|
53
|
-
setIsFullscreen(!isFullscreen)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Show notification with auto-dismiss
|
|
57
|
-
const showNotification = (type: 'success' | 'error', message: string) => {
|
|
58
|
-
setNotification({ type, message })
|
|
59
|
-
setTimeout(() => setNotification(null), 5000)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Export preferences
|
|
63
|
-
const handleExport = () => {
|
|
64
|
-
const configJson = AdminLocalStorage.exportConfig()
|
|
65
|
-
if (!configJson) {
|
|
66
|
-
showNotification('error', 'Failed to export preferences')
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Create download
|
|
71
|
-
const blob = new Blob([configJson], { type: 'application/json' })
|
|
72
|
-
const url = URL.createObjectURL(blob)
|
|
73
|
-
const link = document.createElement('a')
|
|
74
|
-
link.href = url
|
|
75
|
-
link.download = `admin-data-preferences-${new Date().toISOString().split('T')[0]}.json`
|
|
76
|
-
document.body.appendChild(link)
|
|
77
|
-
link.click()
|
|
78
|
-
document.body.removeChild(link)
|
|
79
|
-
URL.revokeObjectURL(url)
|
|
80
|
-
|
|
81
|
-
showNotification('success', 'Preferences exported successfully')
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Import preferences
|
|
85
|
-
const handleImport = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
86
|
-
const file = event.target.files?.[0]
|
|
87
|
-
if (!file) return
|
|
88
|
-
|
|
89
|
-
const reader = new FileReader()
|
|
90
|
-
reader.onload = (e) => {
|
|
91
|
-
const content = e.target?.result as string
|
|
92
|
-
if (!content) {
|
|
93
|
-
showNotification('error', 'Failed to read file')
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const success = AdminLocalStorage.importConfig(content)
|
|
98
|
-
if (success) {
|
|
99
|
-
showNotification('success', 'Preferences imported successfully. Refreshing...')
|
|
100
|
-
// Refresh the page to apply new preferences
|
|
101
|
-
setTimeout(() => window.location.reload(), 1500)
|
|
102
|
-
} else {
|
|
103
|
-
showNotification('error', 'Invalid preferences file')
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
reader.onerror = () => {
|
|
107
|
-
showNotification('error', 'Failed to read file')
|
|
108
|
-
}
|
|
109
|
-
reader.readAsText(file)
|
|
110
|
-
|
|
111
|
-
// Reset file input
|
|
112
|
-
if (fileInputRef.current) {
|
|
113
|
-
fileInputRef.current.value = ''
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const triggerImport = () => {
|
|
118
|
-
fileInputRef.current?.click()
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const content = (
|
|
122
|
-
<div className="max-w-full mx-auto h-full flex flex-col">
|
|
123
|
-
{/* Notification Toast */}
|
|
124
|
-
{notification && (
|
|
125
|
-
<div className={`mb-4 px-4 py-3 rounded-md border ${
|
|
126
|
-
notification.type === 'success'
|
|
127
|
-
? 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200'
|
|
128
|
-
: 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200'
|
|
129
|
-
}`}>
|
|
130
|
-
<div className="flex items-center">
|
|
131
|
-
{notification.type === 'success' ? (
|
|
132
|
-
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
133
|
-
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
|
134
|
-
</svg>
|
|
135
|
-
) : (
|
|
136
|
-
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
137
|
-
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
138
|
-
</svg>
|
|
139
|
-
)}
|
|
140
|
-
<span className="text-sm font-medium">{notification.message}</span>
|
|
141
|
-
</div>
|
|
142
|
-
</div>
|
|
143
|
-
)}
|
|
144
|
-
|
|
145
|
-
{/* Model Selector and Controls */}
|
|
146
|
-
<div className="mb-6 space-y-3">
|
|
147
|
-
<div className="flex items-end gap-3">
|
|
148
|
-
<div className="flex-1">
|
|
149
|
-
<label htmlFor="model-selector" className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
|
|
150
|
-
Select Model
|
|
151
|
-
</label>
|
|
152
|
-
<div className="relative">
|
|
153
|
-
<select
|
|
154
|
-
id="model-selector"
|
|
155
|
-
value={currentModel?.name || ''}
|
|
156
|
-
onChange={handleModelChange}
|
|
157
|
-
className="w-full h-[50px] pl-4 pr-10 py-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-web focus:border-green-web text-base appearance-none cursor-pointer"
|
|
158
|
-
>
|
|
159
|
-
<option value="">Choose a model...</option>
|
|
160
|
-
{sortedModels.map(model => (
|
|
161
|
-
<option key={model.name} value={model.name}>
|
|
162
|
-
{getPluralName(spacedWords(model.name))}
|
|
163
|
-
</option>
|
|
164
|
-
))}
|
|
165
|
-
</select>
|
|
166
|
-
{/* Custom chevron icon */}
|
|
167
|
-
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
|
168
|
-
<svg className="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
169
|
-
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
|
170
|
-
</svg>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
</div>
|
|
174
|
-
|
|
175
|
-
{/* Fullscreen Toggle Button */}
|
|
176
|
-
<button
|
|
177
|
-
onClick={toggleFullscreen}
|
|
178
|
-
className="h-[50px] px-4 py-3 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-green-web focus:border-green-web transition-colors"
|
|
179
|
-
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
|
180
|
-
>
|
|
181
|
-
{isFullscreen ? (
|
|
182
|
-
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
183
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />
|
|
184
|
-
</svg>
|
|
185
|
-
) : (
|
|
186
|
-
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
187
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
|
|
188
|
-
</svg>
|
|
189
|
-
)}
|
|
190
|
-
</button>
|
|
191
|
-
</div>
|
|
192
|
-
|
|
193
|
-
{/* Import/Export Preferences */}
|
|
194
|
-
<div className="flex items-center gap-2 text-sm">
|
|
195
|
-
<span className="text-gray-600 dark:text-gray-400">Preferences:</span>
|
|
196
|
-
<button
|
|
197
|
-
onClick={handleExport}
|
|
198
|
-
className="inline-flex items-center px-3 py-1.5 text-gray-700 dark:text-gray-300 hover:text-green-web dark:hover:text-green-web transition-colors"
|
|
199
|
-
title="Export your preferences to a file"
|
|
200
|
-
>
|
|
201
|
-
<svg className="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
202
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
203
|
-
</svg>
|
|
204
|
-
Export
|
|
205
|
-
</button>
|
|
206
|
-
<span className="text-gray-300 dark:text-gray-600">|</span>
|
|
207
|
-
<button
|
|
208
|
-
onClick={triggerImport}
|
|
209
|
-
className="inline-flex items-center px-3 py-1.5 text-gray-700 dark:text-gray-300 hover:text-green-web dark:hover:text-green-web transition-colors"
|
|
210
|
-
title="Import preferences from a file"
|
|
211
|
-
>
|
|
212
|
-
<svg className="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
213
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
|
214
|
-
</svg>
|
|
215
|
-
Import
|
|
216
|
-
</button>
|
|
217
|
-
<input
|
|
218
|
-
ref={fileInputRef}
|
|
219
|
-
type="file"
|
|
220
|
-
accept="application/json,.json"
|
|
221
|
-
onChange={handleImport}
|
|
222
|
-
className="hidden"
|
|
223
|
-
/>
|
|
224
|
-
</div>
|
|
225
|
-
</div>
|
|
226
|
-
|
|
227
|
-
{/* Page Content */}
|
|
228
|
-
<div className="flex-1 overflow-auto">
|
|
229
|
-
<Outlet />
|
|
230
|
-
</div>
|
|
231
|
-
</div>
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
if (isFullscreen) {
|
|
235
|
-
return (
|
|
236
|
-
<div className="fixed inset-0 z-50 bg-white dark:bg-gray-900 overflow-hidden flex flex-col">
|
|
237
|
-
<div className="h-full w-full overflow-auto p-6 md:p-8 lg:p-12">
|
|
238
|
-
{content}
|
|
239
|
-
</div>
|
|
240
|
-
</div>
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return (
|
|
245
|
-
<div className="h-full overflow-hidden flex flex-col">
|
|
246
|
-
<main className="h-full w-full overflow-auto p-6 md:p-8 lg:p-12">
|
|
247
|
-
{content}
|
|
248
|
-
</main>
|
|
249
|
-
</div>
|
|
250
|
-
)
|
|
251
|
-
}
|