mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 21:44:54 +00:00
Merge pull request #5 from johndoe6345789/copilot/refactor-large-components
Refactor large components into focused modules
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { configureMonacoTypeScript, getMonacoLanguage } from '@/lib/monaco-config'
|
||||
import type { Monaco } from '@monaco-editor/react'
|
||||
|
||||
const Editor = lazy(() => import('@monaco-editor/react'))
|
||||
@@ -21,161 +22,7 @@ function EditorLoadingSkeleton({ height = '400px' }: { height?: string }) {
|
||||
}
|
||||
|
||||
function handleEditorBeforeMount(monaco: Monaco) {
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.Latest,
|
||||
allowNonTsExtensions: true,
|
||||
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
module: monaco.languages.typescript.ModuleKind.ESNext,
|
||||
noEmit: true,
|
||||
esModuleInterop: true,
|
||||
jsx: monaco.languages.typescript.JsxEmit.React,
|
||||
reactNamespace: 'React',
|
||||
allowJs: true,
|
||||
typeRoots: ['node_modules/@types'],
|
||||
})
|
||||
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.Latest,
|
||||
allowNonTsExtensions: true,
|
||||
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
module: monaco.languages.typescript.ModuleKind.ESNext,
|
||||
noEmit: true,
|
||||
esModuleInterop: true,
|
||||
jsx: monaco.languages.typescript.JsxEmit.React,
|
||||
reactNamespace: 'React',
|
||||
allowJs: true,
|
||||
typeRoots: ['node_modules/@types'],
|
||||
})
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
|
||||
noSemanticValidation: false,
|
||||
noSyntaxValidation: false,
|
||||
})
|
||||
|
||||
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
||||
noSemanticValidation: false,
|
||||
noSyntaxValidation: false,
|
||||
})
|
||||
|
||||
const reactTypes = `
|
||||
declare module 'react' {
|
||||
export function useState<T>(initialState: T | (() => T)): [T, (newState: T | ((prevState: T) => T)) => void];
|
||||
export function useEffect(effect: () => void | (() => void), deps?: any[]): void;
|
||||
export function useCallback<T extends (...args: any[]) => any>(callback: T, deps: any[]): T;
|
||||
export function useMemo<T>(factory: () => T, deps: any[]): T;
|
||||
export function useRef<T>(initialValue: T): { current: T };
|
||||
export function useContext<T>(context: React.Context<T>): T;
|
||||
export function useReducer<S, A>(reducer: (state: S, action: A) => S, initialState: S): [S, (action: A) => void];
|
||||
export interface Context<T> { Provider: any; Consumer: any; }
|
||||
export function createContext<T>(defaultValue: T): Context<T>;
|
||||
export type FC<P = {}> = (props: P) => JSX.Element | null;
|
||||
export type ReactNode = JSX.Element | string | number | boolean | null | undefined;
|
||||
export interface CSSProperties { [key: string]: any; }
|
||||
}
|
||||
|
||||
declare namespace JSX {
|
||||
interface Element {}
|
||||
interface IntrinsicElements {
|
||||
[elemName: string]: any;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const shadcnTypes = `
|
||||
declare module '@/components/ui/button' {
|
||||
export interface ButtonProps {
|
||||
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
||||
size?: 'default' | 'sm' | 'lg' | 'icon';
|
||||
asChild?: boolean;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
}
|
||||
export function Button(props: ButtonProps): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@/components/ui/card' {
|
||||
export interface CardProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
export function Card(props: CardProps): JSX.Element;
|
||||
export function CardHeader(props: CardProps): JSX.Element;
|
||||
export function CardTitle(props: CardProps): JSX.Element;
|
||||
export function CardDescription(props: CardProps): JSX.Element;
|
||||
export function CardContent(props: CardProps): JSX.Element;
|
||||
export function CardFooter(props: CardProps): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@/components/ui/input' {
|
||||
export interface InputProps {
|
||||
type?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: (e: any) => void;
|
||||
className?: string;
|
||||
}
|
||||
export function Input(props: InputProps): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@/components/ui/badge' {
|
||||
export interface BadgeProps {
|
||||
variant?: 'default' | 'secondary' | 'destructive' | 'outline';
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
export function Badge(props: BadgeProps): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@/components/ui/checkbox' {
|
||||
export interface CheckboxProps {
|
||||
checked?: boolean;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
className?: string;
|
||||
}
|
||||
export function Checkbox(props: CheckboxProps): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@/components/ui/switch' {
|
||||
export interface SwitchProps {
|
||||
checked?: boolean;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
className?: string;
|
||||
}
|
||||
export function Switch(props: SwitchProps): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@/lib/utils' {
|
||||
export function cn(...inputs: any[]): string;
|
||||
}
|
||||
|
||||
declare module '@phosphor-icons/react' {
|
||||
export interface IconProps {
|
||||
size?: number;
|
||||
weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone';
|
||||
color?: string;
|
||||
className?: string;
|
||||
}
|
||||
export function Plus(props: IconProps): JSX.Element;
|
||||
export function Minus(props: IconProps): JSX.Element;
|
||||
export function Check(props: IconProps): JSX.Element;
|
||||
export function X(props: IconProps): JSX.Element;
|
||||
export function Trash(props: IconProps): JSX.Element;
|
||||
export function PencilSimple(props: IconProps): JSX.Element;
|
||||
export function MagnifyingGlass(props: IconProps): JSX.Element;
|
||||
export function Heart(props: IconProps): JSX.Element;
|
||||
export function Star(props: IconProps): JSX.Element;
|
||||
export function User(props: IconProps): JSX.Element;
|
||||
export function ShoppingCart(props: IconProps): JSX.Element;
|
||||
[key: string]: (props: IconProps) => JSX.Element;
|
||||
}
|
||||
`
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(reactTypes, 'file:///node_modules/@types/react/index.d.ts')
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(reactTypes, 'file:///node_modules/@types/react/index.d.ts')
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(shadcnTypes, 'file:///node_modules/@types/shadcn/index.d.ts')
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(shadcnTypes, 'file:///node_modules/@types/shadcn/index.d.ts')
|
||||
configureMonacoTypeScript(monaco)
|
||||
}
|
||||
|
||||
export function MonacoEditor({
|
||||
@@ -221,29 +68,3 @@ export function MonacoEditor({
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
function getMonacoLanguage(language: string): string {
|
||||
const languageMap: Record<string, string> = {
|
||||
'JavaScript': 'javascript',
|
||||
'TypeScript': 'typescript',
|
||||
'JSX': 'javascript',
|
||||
'TSX': 'typescript',
|
||||
'Python': 'python',
|
||||
'Java': 'java',
|
||||
'C++': 'cpp',
|
||||
'C#': 'csharp',
|
||||
'Ruby': 'ruby',
|
||||
'Go': 'go',
|
||||
'Rust': 'rust',
|
||||
'PHP': 'php',
|
||||
'Swift': 'swift',
|
||||
'Kotlin': 'kotlin',
|
||||
'HTML': 'html',
|
||||
'CSS': 'css',
|
||||
'SQL': 'sql',
|
||||
'Bash': 'shell',
|
||||
'Other': 'plaintext',
|
||||
}
|
||||
|
||||
return languageMap[language] || 'plaintext'
|
||||
}
|
||||
|
||||
284
src/hooks/useSettingsState.ts
Normal file
284
src/hooks/useSettingsState.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
getDatabaseStats,
|
||||
exportDatabase,
|
||||
importDatabase,
|
||||
clearDatabase,
|
||||
seedDatabase,
|
||||
getAllSnippets,
|
||||
validateDatabaseSchema
|
||||
} from '@/lib/db'
|
||||
import {
|
||||
saveStorageConfig,
|
||||
loadStorageConfig,
|
||||
FlaskStorageAdapter,
|
||||
type StorageBackend
|
||||
} from '@/lib/storage'
|
||||
|
||||
export function useSettingsState() {
|
||||
const [stats, setStats] = useState<{
|
||||
snippetCount: number
|
||||
templateCount: number
|
||||
storageType: 'indexeddb' | 'localstorage' | 'none'
|
||||
databaseSize: number
|
||||
} | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [storageBackend, setStorageBackend] = useState<StorageBackend>('indexeddb')
|
||||
const [flaskUrl, setFlaskUrl] = useState('')
|
||||
const [flaskConnectionStatus, setFlaskConnectionStatus] = useState<'unknown' | 'connected' | 'failed'>('unknown')
|
||||
const [testingConnection, setTestingConnection] = useState(false)
|
||||
const [envVarSet, setEnvVarSet] = useState(false)
|
||||
const [schemaHealth, setSchemaHealth] = useState<'unknown' | 'healthy' | 'corrupted'>('unknown')
|
||||
const [checkingSchema, setCheckingSchema] = useState(false)
|
||||
|
||||
const loadStats = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await getDatabaseStats()
|
||||
setStats(data)
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats:', error)
|
||||
toast.error('Failed to load database statistics')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const testFlaskConnection = async (url: string) => {
|
||||
setTestingConnection(true)
|
||||
try {
|
||||
const adapter = new FlaskStorageAdapter(url)
|
||||
const connected = await adapter.testConnection()
|
||||
setFlaskConnectionStatus(connected ? 'connected' : 'failed')
|
||||
return connected
|
||||
} catch (error) {
|
||||
console.error('Connection test failed:', error)
|
||||
setFlaskConnectionStatus('failed')
|
||||
return false
|
||||
} finally {
|
||||
setTestingConnection(false)
|
||||
}
|
||||
}
|
||||
|
||||
const checkSchemaHealth = async () => {
|
||||
setCheckingSchema(true)
|
||||
try {
|
||||
const result = await validateDatabaseSchema()
|
||||
setSchemaHealth(result.valid ? 'healthy' : 'corrupted')
|
||||
|
||||
if (!result.valid) {
|
||||
console.warn('Schema validation failed:', result.issues)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Schema check failed:', error)
|
||||
setSchemaHealth('corrupted')
|
||||
} finally {
|
||||
setCheckingSchema(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadStats()
|
||||
checkSchemaHealth()
|
||||
const config = loadStorageConfig()
|
||||
|
||||
const envFlaskUrl = import.meta.env.VITE_FLASK_BACKEND_URL
|
||||
const isEnvSet = Boolean(envFlaskUrl)
|
||||
setEnvVarSet(isEnvSet)
|
||||
|
||||
setStorageBackend(config.backend)
|
||||
setFlaskUrl(config.flaskUrl || envFlaskUrl || 'http://localhost:5000')
|
||||
}, [])
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
const data = await exportDatabase()
|
||||
const blob = new Blob([new Uint8Array(data)], { type: 'application/octet-stream' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `codesnippet-backup-${Date.now()}.db`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
toast.success('Database exported successfully')
|
||||
} catch (error) {
|
||||
console.error('Failed to export:', error)
|
||||
toast.error('Failed to export database')
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const data = new Uint8Array(arrayBuffer)
|
||||
await importDatabase(data)
|
||||
toast.success('Database imported successfully')
|
||||
await loadStats()
|
||||
} catch (error) {
|
||||
console.error('Failed to import:', error)
|
||||
toast.error('Failed to import database')
|
||||
}
|
||||
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
const handleClear = async () => {
|
||||
if (!confirm('Are you sure you want to clear all data? This cannot be undone.')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await clearDatabase()
|
||||
toast.success('Database cleared and schema recreated successfully')
|
||||
await loadStats()
|
||||
await checkSchemaHealth()
|
||||
} catch (error) {
|
||||
console.error('Failed to clear:', error)
|
||||
toast.error('Failed to clear database')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSeed = async () => {
|
||||
try {
|
||||
await seedDatabase()
|
||||
toast.success('Sample data added successfully')
|
||||
await loadStats()
|
||||
} catch (error) {
|
||||
console.error('Failed to seed:', error)
|
||||
toast.error('Failed to add sample data')
|
||||
}
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
await testFlaskConnection(flaskUrl)
|
||||
}
|
||||
|
||||
const handleSaveStorageConfig = async () => {
|
||||
if (storageBackend === 'flask') {
|
||||
if (!flaskUrl) {
|
||||
toast.error('Please enter a Flask backend URL')
|
||||
return
|
||||
}
|
||||
|
||||
const connected = await testFlaskConnection(flaskUrl)
|
||||
if (!connected) {
|
||||
toast.error('Cannot connect to Flask backend. Please check the URL and ensure the server is running.')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
saveStorageConfig({
|
||||
backend: storageBackend,
|
||||
flaskUrl: storageBackend === 'flask' ? flaskUrl : undefined
|
||||
})
|
||||
|
||||
toast.success('Storage backend updated successfully')
|
||||
await loadStats()
|
||||
}
|
||||
|
||||
const handleMigrateToFlask = async () => {
|
||||
if (!flaskUrl) {
|
||||
toast.error('Please enter a Flask backend URL')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const adapter = new FlaskStorageAdapter(flaskUrl)
|
||||
const connected = await adapter.testConnection()
|
||||
|
||||
if (!connected) {
|
||||
toast.error('Cannot connect to Flask backend')
|
||||
return
|
||||
}
|
||||
|
||||
const snippets = await getAllSnippets()
|
||||
|
||||
if (snippets.length === 0) {
|
||||
toast.info('No snippets to migrate')
|
||||
return
|
||||
}
|
||||
|
||||
await adapter.migrateFromIndexedDB(snippets)
|
||||
|
||||
saveStorageConfig({
|
||||
backend: 'flask',
|
||||
flaskUrl
|
||||
})
|
||||
|
||||
toast.success(`Successfully migrated ${snippets.length} snippets to Flask backend`)
|
||||
await loadStats()
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error)
|
||||
toast.error('Failed to migrate data to Flask backend')
|
||||
}
|
||||
}
|
||||
|
||||
const handleMigrateToIndexedDB = async () => {
|
||||
if (!flaskUrl) {
|
||||
toast.error('Please enter a Flask backend URL')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const adapter = new FlaskStorageAdapter(flaskUrl)
|
||||
const snippets = await adapter.migrateToIndexedDB()
|
||||
|
||||
if (snippets.length === 0) {
|
||||
toast.info('No snippets to migrate')
|
||||
return
|
||||
}
|
||||
|
||||
saveStorageConfig({
|
||||
backend: 'indexeddb'
|
||||
})
|
||||
|
||||
// Full page reload is necessary here to reinitialize the database layer
|
||||
// with the new backend after migration from Flask to IndexedDB
|
||||
window.location.reload()
|
||||
|
||||
toast.success(`Successfully migrated ${snippets.length} snippets to IndexedDB`)
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error)
|
||||
toast.error('Failed to migrate data from Flask backend')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
stats,
|
||||
loading,
|
||||
storageBackend,
|
||||
setStorageBackend,
|
||||
flaskUrl,
|
||||
setFlaskUrl,
|
||||
flaskConnectionStatus,
|
||||
setFlaskConnectionStatus,
|
||||
testingConnection,
|
||||
envVarSet,
|
||||
schemaHealth,
|
||||
checkingSchema,
|
||||
handleExport,
|
||||
handleImport,
|
||||
handleClear,
|
||||
handleSeed,
|
||||
formatBytes,
|
||||
handleTestConnection,
|
||||
handleSaveStorageConfig,
|
||||
handleMigrateToFlask,
|
||||
handleMigrateToIndexedDB,
|
||||
checkSchemaHealth,
|
||||
}
|
||||
}
|
||||
52
src/lib/db-mapper.ts
Normal file
52
src/lib/db-mapper.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Database row-to-object mapping utilities
|
||||
* Handles conversion of SQL query results to typed objects
|
||||
*/
|
||||
|
||||
/**
|
||||
* Maps a SQL query result row to a typed object
|
||||
* Handles special conversions for boolean and JSON fields
|
||||
*/
|
||||
export function mapRowToObject<T>(row: any[], columns: string[]): T {
|
||||
const obj: any = {}
|
||||
|
||||
columns.forEach((col, idx) => {
|
||||
const value = row[idx]
|
||||
|
||||
// Convert integer boolean fields to actual booleans
|
||||
if (col === 'hasPreview' || col === 'isDefault') {
|
||||
obj[col] = value === 1
|
||||
}
|
||||
// Parse JSON string fields with error handling
|
||||
else if (col === 'inputParameters') {
|
||||
if (value) {
|
||||
try {
|
||||
obj[col] = JSON.parse(value as string)
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse JSON for ${col}:`, error)
|
||||
obj[col] = undefined
|
||||
}
|
||||
} else {
|
||||
obj[col] = undefined
|
||||
}
|
||||
}
|
||||
// All other fields pass through as-is
|
||||
else {
|
||||
obj[col] = value
|
||||
}
|
||||
})
|
||||
|
||||
return obj as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps multiple SQL result rows to an array of typed objects
|
||||
*/
|
||||
export function mapRowsToObjects<T>(results: any[]): T[] {
|
||||
if (results.length === 0) return []
|
||||
|
||||
const columns = results[0].columns
|
||||
const values = results[0].values
|
||||
|
||||
return values.map(row => mapRowToObject<T>(row, columns))
|
||||
}
|
||||
105
src/lib/db.ts
105
src/lib/db.ts
@@ -1,6 +1,7 @@
|
||||
import initSqlJs, { Database } from 'sql.js'
|
||||
import type { Snippet, SnippetTemplate } from './types'
|
||||
import { getStorageConfig, FlaskStorageAdapter, loadStorageConfig } from './storage'
|
||||
import { mapRowToObject, mapRowsToObjects } from './db-mapper'
|
||||
|
||||
let dbInstance: Database | null = null
|
||||
let sqlInstance: any = null
|
||||
@@ -304,27 +305,9 @@ export async function getAllSnippets(): Promise<Snippet[]> {
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
|
||||
const results = db.exec('SELECT * FROM snippets ORDER BY updatedAt DESC')
|
||||
|
||||
if (results.length === 0) return []
|
||||
|
||||
const columns = results[0].columns
|
||||
const values = results[0].values
|
||||
|
||||
return values.map(row => {
|
||||
const snippet: any = {}
|
||||
columns.forEach((col, idx) => {
|
||||
if (col === 'hasPreview') {
|
||||
snippet[col] = row[idx] === 1
|
||||
} else if (col === 'inputParameters') {
|
||||
snippet[col] = row[idx] ? JSON.parse(row[idx] as string) : undefined
|
||||
} else {
|
||||
snippet[col] = row[idx]
|
||||
}
|
||||
})
|
||||
return snippet as Snippet
|
||||
})
|
||||
return mapRowsToObjects<Snippet>(results)
|
||||
}
|
||||
|
||||
export async function getSnippet(id: string): Promise<Snippet | null> {
|
||||
@@ -334,7 +317,6 @@ export async function getSnippet(id: string): Promise<Snippet | null> {
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
|
||||
const results = db.exec('SELECT * FROM snippets WHERE id = ?', [id])
|
||||
|
||||
if (results.length === 0 || results[0].values.length === 0) return null
|
||||
@@ -342,18 +324,7 @@ export async function getSnippet(id: string): Promise<Snippet | null> {
|
||||
const columns = results[0].columns
|
||||
const row = results[0].values[0]
|
||||
|
||||
const snippet: any = {}
|
||||
columns.forEach((col, idx) => {
|
||||
if (col === 'hasPreview') {
|
||||
snippet[col] = row[idx] === 1
|
||||
} else if (col === 'inputParameters') {
|
||||
snippet[col] = row[idx] ? JSON.parse(row[idx] as string) : undefined
|
||||
} else {
|
||||
snippet[col] = row[idx]
|
||||
}
|
||||
})
|
||||
|
||||
return snippet as Snippet
|
||||
return mapRowToObject<Snippet>(row, columns)
|
||||
}
|
||||
|
||||
export async function createSnippet(snippet: Snippet): Promise<void> {
|
||||
@@ -431,27 +402,9 @@ export async function deleteSnippet(id: string): Promise<void> {
|
||||
|
||||
export async function getAllTemplates(): Promise<SnippetTemplate[]> {
|
||||
const db = await initDB()
|
||||
|
||||
const results = db.exec('SELECT * FROM snippet_templates')
|
||||
|
||||
if (results.length === 0) return []
|
||||
|
||||
const columns = results[0].columns
|
||||
const values = results[0].values
|
||||
|
||||
return values.map(row => {
|
||||
const template: any = {}
|
||||
columns.forEach((col, idx) => {
|
||||
if (col === 'hasPreview') {
|
||||
template[col] = row[idx] === 1
|
||||
} else if (col === 'inputParameters') {
|
||||
template[col] = row[idx] ? JSON.parse(row[idx] as string) : undefined
|
||||
} else {
|
||||
template[col] = row[idx]
|
||||
}
|
||||
})
|
||||
return template as SnippetTemplate
|
||||
})
|
||||
return mapRowsToObjects<SnippetTemplate>(results)
|
||||
}
|
||||
|
||||
export async function createTemplate(template: SnippetTemplate): Promise<void> {
|
||||
@@ -957,25 +910,9 @@ export async function getAllNamespaces(): Promise<import('./types').Namespace[]>
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
|
||||
const results = db.exec('SELECT * FROM namespaces ORDER BY isDefault DESC, name ASC')
|
||||
|
||||
if (results.length === 0) return []
|
||||
|
||||
const columns = results[0].columns
|
||||
const values = results[0].values
|
||||
|
||||
return values.map(row => {
|
||||
const namespace: any = {}
|
||||
columns.forEach((col, idx) => {
|
||||
if (col === 'isDefault') {
|
||||
namespace[col] = row[idx] === 1
|
||||
} else {
|
||||
namespace[col] = row[idx]
|
||||
}
|
||||
})
|
||||
return namespace
|
||||
})
|
||||
return mapRowsToObjects<import('./types').Namespace>(results)
|
||||
}
|
||||
|
||||
export async function createNamespace(name: string): Promise<import('./types').Namespace> {
|
||||
@@ -1057,32 +994,13 @@ export async function ensureDefaultNamespace(): Promise<void> {
|
||||
|
||||
export async function getSnippetsByNamespace(namespaceId: string): Promise<Snippet[]> {
|
||||
const db = await initDB()
|
||||
|
||||
const results = db.exec('SELECT * FROM snippets WHERE namespaceId = ? OR (namespaceId IS NULL AND ? = (SELECT id FROM namespaces WHERE isDefault = 1)) ORDER BY updatedAt DESC', [namespaceId, namespaceId])
|
||||
|
||||
if (results.length === 0) return []
|
||||
|
||||
const columns = results[0].columns
|
||||
const values = results[0].values
|
||||
|
||||
return values.map(row => {
|
||||
const snippet: any = {}
|
||||
columns.forEach((col, idx) => {
|
||||
if (col === 'hasPreview') {
|
||||
snippet[col] = row[idx] === 1
|
||||
} else if (col === 'inputParameters') {
|
||||
snippet[col] = row[idx] ? JSON.parse(row[idx] as string) : undefined
|
||||
} else {
|
||||
snippet[col] = row[idx]
|
||||
}
|
||||
})
|
||||
return snippet as Snippet
|
||||
})
|
||||
return mapRowsToObjects<Snippet>(results)
|
||||
}
|
||||
|
||||
export async function getNamespaceById(id: string): Promise<import('./types').Namespace | null> {
|
||||
const db = await initDB()
|
||||
|
||||
const results = db.exec('SELECT * FROM namespaces WHERE id = ?', [id])
|
||||
|
||||
if (results.length === 0 || results[0].values.length === 0) return null
|
||||
@@ -1090,16 +1008,7 @@ export async function getNamespaceById(id: string): Promise<import('./types').Na
|
||||
const columns = results[0].columns
|
||||
const row = results[0].values[0]
|
||||
|
||||
const namespace: any = {}
|
||||
columns.forEach((col, idx) => {
|
||||
if (col === 'isDefault') {
|
||||
namespace[col] = row[idx] === 1
|
||||
} else {
|
||||
namespace[col] = row[idx]
|
||||
}
|
||||
})
|
||||
|
||||
return namespace
|
||||
return mapRowToObject<import('./types').Namespace>(row, columns)
|
||||
}
|
||||
|
||||
export async function moveSnippetToNamespace(snippetId: string, targetNamespaceId: string): Promise<void> {
|
||||
|
||||
213
src/lib/monaco-config.ts
Normal file
213
src/lib/monaco-config.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Monaco Editor TypeScript/React type definitions and compiler options
|
||||
* These definitions are loaded into Monaco to provide IntelliSense for React and shadcn components
|
||||
*/
|
||||
|
||||
import type { Monaco } from '@monaco-editor/react'
|
||||
|
||||
/**
|
||||
* TypeScript compiler options for both TypeScript and JavaScript files
|
||||
* Note: Values are enum numbers since Monaco enums aren't directly importable:
|
||||
* - target: 2 = ScriptTarget.Latest
|
||||
* - moduleResolution: 2 = ModuleResolutionKind.NodeJs
|
||||
* - module: 99 = ModuleKind.ESNext
|
||||
* - jsx: 2 = JsxEmit.React
|
||||
*/
|
||||
export const compilerOptions = {
|
||||
target: 2, // ScriptTarget.Latest
|
||||
allowNonTsExtensions: true,
|
||||
moduleResolution: 2, // ModuleResolutionKind.NodeJs
|
||||
module: 99, // ModuleKind.ESNext
|
||||
noEmit: true,
|
||||
esModuleInterop: true,
|
||||
jsx: 2, // JsxEmit.React
|
||||
reactNamespace: 'React',
|
||||
allowJs: true,
|
||||
typeRoots: ['node_modules/@types'],
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnostics options for TypeScript/JavaScript validation
|
||||
*/
|
||||
export const diagnosticsOptions = {
|
||||
noSemanticValidation: false,
|
||||
noSyntaxValidation: false,
|
||||
}
|
||||
|
||||
/**
|
||||
* React type definitions for Monaco IntelliSense
|
||||
*/
|
||||
export const reactTypes = `
|
||||
declare module 'react' {
|
||||
export function useState<T>(initialState: T | (() => T)): [T, (newState: T | ((prevState: T) => T)) => void];
|
||||
export function useEffect(effect: () => void | (() => void), deps?: any[]): void;
|
||||
export function useCallback<T extends (...args: any[]) => any>(callback: T, deps: any[]): T;
|
||||
export function useMemo<T>(factory: () => T, deps: any[]): T;
|
||||
export function useRef<T>(initialValue: T): { current: T };
|
||||
export function useContext<T>(context: React.Context<T>): T;
|
||||
export function useReducer<S, A>(reducer: (state: S, action: A) => S, initialState: S): [S, (action: A) => void];
|
||||
export interface Context<T> { Provider: any; Consumer: any; }
|
||||
export function createContext<T>(defaultValue: T): Context<T>;
|
||||
export type FC<P = {}> = (props: P) => JSX.Element | null;
|
||||
export type ReactNode = JSX.Element | string | number | boolean | null | undefined;
|
||||
export interface CSSProperties { [key: string]: any; }
|
||||
}
|
||||
|
||||
declare namespace JSX {
|
||||
interface Element {}
|
||||
interface IntrinsicElements {
|
||||
[elemName: string]: any;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* shadcn/ui component type definitions for Monaco IntelliSense
|
||||
*/
|
||||
export const shadcnTypes = `
|
||||
declare module '@/components/ui/button' {
|
||||
export interface ButtonProps {
|
||||
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
||||
size?: 'default' | 'sm' | 'lg' | 'icon';
|
||||
asChild?: boolean;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
}
|
||||
export function Button(props: ButtonProps): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@/components/ui/card' {
|
||||
export interface CardProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
export function Card(props: CardProps): JSX.Element;
|
||||
export function CardHeader(props: CardProps): JSX.Element;
|
||||
export function CardTitle(props: CardProps): JSX.Element;
|
||||
export function CardDescription(props: CardProps): JSX.Element;
|
||||
export function CardContent(props: CardProps): JSX.Element;
|
||||
export function CardFooter(props: CardProps): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@/components/ui/input' {
|
||||
export interface InputProps {
|
||||
type?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: (e: any) => void;
|
||||
className?: string;
|
||||
}
|
||||
export function Input(props: InputProps): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@/components/ui/badge' {
|
||||
export interface BadgeProps {
|
||||
variant?: 'default' | 'secondary' | 'destructive' | 'outline';
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
export function Badge(props: BadgeProps): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@/components/ui/checkbox' {
|
||||
export interface CheckboxProps {
|
||||
checked?: boolean;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
className?: string;
|
||||
}
|
||||
export function Checkbox(props: CheckboxProps): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@/components/ui/switch' {
|
||||
export interface SwitchProps {
|
||||
checked?: boolean;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
className?: string;
|
||||
}
|
||||
export function Switch(props: SwitchProps): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@/lib/utils' {
|
||||
export function cn(...inputs: any[]): string;
|
||||
}
|
||||
|
||||
declare module '@phosphor-icons/react' {
|
||||
export interface IconProps {
|
||||
size?: number;
|
||||
weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone';
|
||||
color?: string;
|
||||
className?: string;
|
||||
}
|
||||
export function Plus(props: IconProps): JSX.Element;
|
||||
export function Minus(props: IconProps): JSX.Element;
|
||||
export function Check(props: IconProps): JSX.Element;
|
||||
export function X(props: IconProps): JSX.Element;
|
||||
export function Trash(props: IconProps): JSX.Element;
|
||||
export function PencilSimple(props: IconProps): JSX.Element;
|
||||
export function MagnifyingGlass(props: IconProps): JSX.Element;
|
||||
export function Heart(props: IconProps): JSX.Element;
|
||||
export function Star(props: IconProps): JSX.Element;
|
||||
export function User(props: IconProps): JSX.Element;
|
||||
export function ShoppingCart(props: IconProps): JSX.Element;
|
||||
[key: string]: (props: IconProps) => JSX.Element;
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* Configures Monaco editor with TypeScript/React support
|
||||
*/
|
||||
export function configureMonacoTypeScript(monaco: Monaco) {
|
||||
// Set compiler options for TypeScript
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions as any)
|
||||
|
||||
// Set compiler options for JavaScript
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions as any)
|
||||
|
||||
// Set diagnostics options for TypeScript
|
||||
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(diagnosticsOptions)
|
||||
|
||||
// Set diagnostics options for JavaScript
|
||||
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(diagnosticsOptions)
|
||||
|
||||
// Add React type definitions
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(reactTypes, 'file:///node_modules/@types/react/index.d.ts')
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(reactTypes, 'file:///node_modules/@types/react/index.d.ts')
|
||||
|
||||
// Add shadcn component type definitions
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(shadcnTypes, 'file:///node_modules/@types/shadcn/index.d.ts')
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(shadcnTypes, 'file:///node_modules/@types/shadcn/index.d.ts')
|
||||
}
|
||||
|
||||
/**
|
||||
* Language mapping for Monaco editor
|
||||
*/
|
||||
export const languageMap: Record<string, string> = {
|
||||
'JavaScript': 'javascript',
|
||||
'TypeScript': 'typescript',
|
||||
'JSX': 'javascript',
|
||||
'TSX': 'typescript',
|
||||
'Python': 'python',
|
||||
'Java': 'java',
|
||||
'C++': 'cpp',
|
||||
'C#': 'csharp',
|
||||
'Ruby': 'ruby',
|
||||
'Go': 'go',
|
||||
'Rust': 'rust',
|
||||
'PHP': 'php',
|
||||
'Swift': 'swift',
|
||||
'Kotlin': 'kotlin',
|
||||
'HTML': 'html',
|
||||
'CSS': 'css',
|
||||
'SQL': 'sql',
|
||||
'Bash': 'shell',
|
||||
'Other': 'plaintext',
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a high-level language name to Monaco language ID
|
||||
*/
|
||||
export function getMonacoLanguage(language: string): string {
|
||||
return languageMap[language] || 'plaintext'
|
||||
}
|
||||
@@ -1,13 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { getDatabaseStats, exportDatabase, importDatabase, clearDatabase, seedDatabase, getAllSnippets, validateDatabaseSchema } from '@/lib/db'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
saveStorageConfig,
|
||||
loadStorageConfig,
|
||||
FlaskStorageAdapter,
|
||||
type StorageBackend
|
||||
} from '@/lib/storage'
|
||||
import { PersistenceSettings } from '@/components/demo/PersistenceSettings'
|
||||
import { SchemaHealthCard } from '@/components/settings/SchemaHealthCard'
|
||||
import { BackendAutoConfigCard } from '@/components/settings/BackendAutoConfigCard'
|
||||
@@ -15,245 +6,33 @@ import { StorageBackendCard } from '@/components/settings/StorageBackendCard'
|
||||
import { DatabaseStatsCard } from '@/components/settings/DatabaseStatsCard'
|
||||
import { StorageInfoCard } from '@/components/settings/StorageInfoCard'
|
||||
import { DatabaseActionsCard } from '@/components/settings/DatabaseActionsCard'
|
||||
import { useSettingsState } from '@/hooks/useSettingsState'
|
||||
|
||||
export function SettingsPage() {
|
||||
const [stats, setStats] = useState<{
|
||||
snippetCount: number
|
||||
templateCount: number
|
||||
storageType: 'indexeddb' | 'localstorage' | 'none'
|
||||
databaseSize: number
|
||||
} | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [storageBackend, setStorageBackend] = useState<StorageBackend>('indexeddb')
|
||||
const [flaskUrl, setFlaskUrl] = useState('')
|
||||
const [flaskConnectionStatus, setFlaskConnectionStatus] = useState<'unknown' | 'connected' | 'failed'>('unknown')
|
||||
const [testingConnection, setTestingConnection] = useState(false)
|
||||
const [envVarSet, setEnvVarSet] = useState(false)
|
||||
const [schemaHealth, setSchemaHealth] = useState<'unknown' | 'healthy' | 'corrupted'>('unknown')
|
||||
const [checkingSchema, setCheckingSchema] = useState(false)
|
||||
|
||||
const loadStats = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await getDatabaseStats()
|
||||
setStats(data)
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats:', error)
|
||||
toast.error('Failed to load database statistics')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const testFlaskConnection = async (url: string) => {
|
||||
setTestingConnection(true)
|
||||
try {
|
||||
const adapter = new FlaskStorageAdapter(url)
|
||||
const connected = await adapter.testConnection()
|
||||
setFlaskConnectionStatus(connected ? 'connected' : 'failed')
|
||||
return connected
|
||||
} catch (error) {
|
||||
console.error('Connection test failed:', error)
|
||||
setFlaskConnectionStatus('failed')
|
||||
return false
|
||||
} finally {
|
||||
setTestingConnection(false)
|
||||
}
|
||||
}
|
||||
|
||||
const checkSchemaHealth = async () => {
|
||||
setCheckingSchema(true)
|
||||
try {
|
||||
const result = await validateDatabaseSchema()
|
||||
setSchemaHealth(result.valid ? 'healthy' : 'corrupted')
|
||||
|
||||
if (!result.valid) {
|
||||
console.warn('Schema validation failed:', result.issues)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Schema check failed:', error)
|
||||
setSchemaHealth('corrupted')
|
||||
} finally {
|
||||
setCheckingSchema(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadStats()
|
||||
checkSchemaHealth()
|
||||
const config = loadStorageConfig()
|
||||
|
||||
const envFlaskUrl = import.meta.env.VITE_FLASK_BACKEND_URL
|
||||
const isEnvSet = Boolean(envFlaskUrl)
|
||||
setEnvVarSet(isEnvSet)
|
||||
|
||||
setStorageBackend(config.backend)
|
||||
setFlaskUrl(config.flaskUrl || envFlaskUrl || 'http://localhost:5000')
|
||||
}, [])
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
const data = await exportDatabase()
|
||||
const blob = new Blob([new Uint8Array(data)], { type: 'application/octet-stream' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `codesnippet-backup-${Date.now()}.db`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
toast.success('Database exported successfully')
|
||||
} catch (error) {
|
||||
console.error('Failed to export:', error)
|
||||
toast.error('Failed to export database')
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const data = new Uint8Array(arrayBuffer)
|
||||
await importDatabase(data)
|
||||
toast.success('Database imported successfully')
|
||||
await loadStats()
|
||||
} catch (error) {
|
||||
console.error('Failed to import:', error)
|
||||
toast.error('Failed to import database')
|
||||
}
|
||||
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
const handleClear = async () => {
|
||||
if (!confirm('Are you sure you want to clear all data? This cannot be undone.')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await clearDatabase()
|
||||
toast.success('Database cleared and schema recreated successfully')
|
||||
await loadStats()
|
||||
await checkSchemaHealth()
|
||||
} catch (error) {
|
||||
console.error('Failed to clear:', error)
|
||||
toast.error('Failed to clear database')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSeed = async () => {
|
||||
try {
|
||||
await seedDatabase()
|
||||
toast.success('Sample data added successfully')
|
||||
await loadStats()
|
||||
} catch (error) {
|
||||
console.error('Failed to seed:', error)
|
||||
toast.error('Failed to add sample data')
|
||||
}
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
await testFlaskConnection(flaskUrl)
|
||||
}
|
||||
|
||||
const handleSaveStorageConfig = async () => {
|
||||
if (storageBackend === 'flask') {
|
||||
if (!flaskUrl) {
|
||||
toast.error('Please enter a Flask backend URL')
|
||||
return
|
||||
}
|
||||
|
||||
const connected = await testFlaskConnection(flaskUrl)
|
||||
if (!connected) {
|
||||
toast.error('Cannot connect to Flask backend. Please check the URL and ensure the server is running.')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
saveStorageConfig({
|
||||
backend: storageBackend,
|
||||
flaskUrl: storageBackend === 'flask' ? flaskUrl : undefined
|
||||
})
|
||||
|
||||
toast.success('Storage backend updated successfully')
|
||||
await loadStats()
|
||||
}
|
||||
|
||||
const handleMigrateToFlask = async () => {
|
||||
if (!flaskUrl) {
|
||||
toast.error('Please enter a Flask backend URL')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const adapter = new FlaskStorageAdapter(flaskUrl)
|
||||
const connected = await adapter.testConnection()
|
||||
|
||||
if (!connected) {
|
||||
toast.error('Cannot connect to Flask backend')
|
||||
return
|
||||
}
|
||||
|
||||
const snippets = await getAllSnippets()
|
||||
|
||||
if (snippets.length === 0) {
|
||||
toast.info('No snippets to migrate')
|
||||
return
|
||||
}
|
||||
|
||||
await adapter.migrateFromIndexedDB(snippets)
|
||||
|
||||
saveStorageConfig({
|
||||
backend: 'flask',
|
||||
flaskUrl
|
||||
})
|
||||
|
||||
toast.success(`Successfully migrated ${snippets.length} snippets to Flask backend`)
|
||||
await loadStats()
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error)
|
||||
toast.error('Failed to migrate data to Flask backend')
|
||||
}
|
||||
}
|
||||
|
||||
const handleMigrateToIndexedDB = async () => {
|
||||
if (!flaskUrl) {
|
||||
toast.error('Please enter a Flask backend URL')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const adapter = new FlaskStorageAdapter(flaskUrl)
|
||||
const snippets = await adapter.migrateToIndexedDB()
|
||||
|
||||
if (snippets.length === 0) {
|
||||
toast.info('No snippets to migrate')
|
||||
return
|
||||
}
|
||||
|
||||
saveStorageConfig({
|
||||
backend: 'indexeddb'
|
||||
})
|
||||
|
||||
window.location.reload()
|
||||
|
||||
toast.success(`Successfully migrated ${snippets.length} snippets to IndexedDB`)
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error)
|
||||
toast.error('Failed to migrate data from Flask backend')
|
||||
}
|
||||
}
|
||||
const {
|
||||
stats,
|
||||
loading,
|
||||
storageBackend,
|
||||
setStorageBackend,
|
||||
flaskUrl,
|
||||
setFlaskUrl,
|
||||
flaskConnectionStatus,
|
||||
setFlaskConnectionStatus,
|
||||
testingConnection,
|
||||
envVarSet,
|
||||
schemaHealth,
|
||||
checkingSchema,
|
||||
handleExport,
|
||||
handleImport,
|
||||
handleClear,
|
||||
handleSeed,
|
||||
formatBytes,
|
||||
handleTestConnection,
|
||||
handleSaveStorageConfig,
|
||||
handleMigrateToFlask,
|
||||
handleMigrateToIndexedDB,
|
||||
checkSchemaHealth,
|
||||
} = useSettingsState()
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
|
||||
Reference in New Issue
Block a user