mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Merge pull request #50 from johndoe6345789/codex/refactor-storagesettings-components
Refactor storage settings UI into shared config and hooks
This commit is contained in:
@@ -1,12 +1,186 @@
|
||||
import { useState } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useStorageBackend } from '@/hooks/use-unified-storage'
|
||||
import { Database, HardDrive, Cloud, Download, Upload, CircleNotch, CloudArrowUp } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Database,
|
||||
HardDrive,
|
||||
Cloud,
|
||||
Download,
|
||||
Upload,
|
||||
CircleNotch,
|
||||
CloudArrowUp,
|
||||
} from '@phosphor-icons/react'
|
||||
import { storageSettingsCopy, getBackendCopy, type StorageBackendKey } from '@/components/storage/storageSettingsConfig'
|
||||
import { useStorageSwitchHandlers } from '@/components/storage/useStorageSwitchHandlers'
|
||||
import { useStorageDataHandlers } from '@/components/storage/useStorageDataHandlers'
|
||||
|
||||
const getBackendIcon = (backend: StorageBackendKey | null) => {
|
||||
switch (backend) {
|
||||
case 'flask':
|
||||
return <CloudArrowUp className="w-5 h-5" />
|
||||
case 'sqlite':
|
||||
return <HardDrive className="w-5 h-5" />
|
||||
case 'indexeddb':
|
||||
return <Database className="w-5 h-5" />
|
||||
case 'sparkkv':
|
||||
return <Cloud className="w-5 h-5" />
|
||||
default:
|
||||
return <Database className="w-5 h-5" />
|
||||
}
|
||||
}
|
||||
|
||||
const StorageSettingsPanelHeader = ({ description }: { description: string }) => (
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="w-5 h-5" />
|
||||
{storageSettingsCopy.panel.title}
|
||||
</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</CardHeader>
|
||||
)
|
||||
|
||||
const BackendStatusSummary = ({ backend }: { backend: StorageBackendKey | null }) => {
|
||||
const backendCopy = getBackendCopy(backend)
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{storageSettingsCopy.panel.currentBackendLabel}</span>
|
||||
{getBackendIcon(backend)}
|
||||
<span className="text-sm">{backendCopy.panelLabel}</span>
|
||||
</div>
|
||||
<Badge variant="secondary">{backend?.toUpperCase() || backendCopy.badgeLabel.toUpperCase()}</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{backendCopy.panelDescription}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type BackendSwitcherProps = {
|
||||
backend: StorageBackendKey | null
|
||||
flaskUrl: string
|
||||
isSwitching: boolean
|
||||
onFlaskUrlChange: (value: string) => void
|
||||
onSwitchToIndexedDB: () => void
|
||||
onSwitchToFlask: () => void
|
||||
onSwitchToSQLite: () => void
|
||||
}
|
||||
|
||||
const BackendSwitcher = ({
|
||||
backend,
|
||||
flaskUrl,
|
||||
isSwitching,
|
||||
onFlaskUrlChange,
|
||||
onSwitchToIndexedDB,
|
||||
onSwitchToFlask,
|
||||
onSwitchToSQLite,
|
||||
}: BackendSwitcherProps) => (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium">{storageSettingsCopy.panel.switchTitle}</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="flask-url" className="text-sm">
|
||||
{storageSettingsCopy.panel.flaskUrlLabel}
|
||||
</Label>
|
||||
<Input
|
||||
id="flask-url"
|
||||
type="text"
|
||||
value={flaskUrl}
|
||||
onChange={(e) => onFlaskUrlChange(e.target.value)}
|
||||
placeholder={storageSettingsCopy.panel.flaskUrlPlaceholder}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">{storageSettingsCopy.panel.flaskUrlHelp}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={onSwitchToIndexedDB}
|
||||
disabled={backend === 'indexeddb' || isSwitching}
|
||||
variant={backend === 'indexeddb' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
>
|
||||
{isSwitching ? (
|
||||
<CircleNotch className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Database className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{storageSettingsCopy.panel.buttons.indexeddb}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onSwitchToFlask}
|
||||
disabled={backend === 'flask' || isSwitching}
|
||||
variant={backend === 'flask' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
>
|
||||
{isSwitching ? (
|
||||
<CircleNotch className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<CloudArrowUp className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{storageSettingsCopy.panel.buttons.flask}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onSwitchToSQLite}
|
||||
disabled={backend === 'sqlite' || isSwitching}
|
||||
variant={backend === 'sqlite' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
>
|
||||
{isSwitching ? (
|
||||
<CircleNotch className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<HardDrive className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{storageSettingsCopy.panel.buttons.sqlite}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{storageSettingsCopy.panel.switchHelp}</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
type DataManagementSectionProps = {
|
||||
isExporting: boolean
|
||||
isImporting: boolean
|
||||
onExport: () => void
|
||||
onImport: () => void
|
||||
}
|
||||
|
||||
const DataManagementSection = ({
|
||||
isExporting,
|
||||
isImporting,
|
||||
onExport,
|
||||
onImport,
|
||||
}: DataManagementSectionProps) => (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium">{storageSettingsCopy.panel.dataTitle}</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button onClick={onExport} disabled={isExporting} variant="outline" size="sm">
|
||||
{isExporting ? (
|
||||
<CircleNotch className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{storageSettingsCopy.panel.buttons.export}
|
||||
</Button>
|
||||
<Button onClick={onImport} disabled={isImporting} variant="outline" size="sm">
|
||||
{isImporting ? (
|
||||
<CircleNotch className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{storageSettingsCopy.panel.buttons.import}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{storageSettingsCopy.panel.dataHelp}</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
export function StorageSettingsPanel() {
|
||||
const {
|
||||
@@ -19,168 +193,33 @@ export function StorageSettingsPanel() {
|
||||
importData,
|
||||
} = useStorageBackend()
|
||||
|
||||
const [isSwitching, setIsSwitching] = useState(false)
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
const [isImporting, setIsImporting] = useState(false)
|
||||
const [flaskUrl, setFlaskUrl] = useState(localStorage.getItem('codeforge-flask-url') || 'http://localhost:5001')
|
||||
const [flaskUrl, setFlaskUrl] = useState(
|
||||
localStorage.getItem('codeforge-flask-url') || storageSettingsCopy.panel.flaskUrlPlaceholder
|
||||
)
|
||||
|
||||
const handleSwitchToFlask = async () => {
|
||||
if (backend === 'flask') {
|
||||
toast.info('Already using Flask backend')
|
||||
return
|
||||
}
|
||||
const { isSwitching, handleSwitchToFlask, handleSwitchToSQLite, handleSwitchToIndexedDB } =
|
||||
useStorageSwitchHandlers({
|
||||
backend,
|
||||
flaskUrl,
|
||||
switchToFlask,
|
||||
switchToSQLite,
|
||||
switchToIndexedDB,
|
||||
})
|
||||
|
||||
if (!flaskUrl) {
|
||||
toast.error('Please enter a Flask backend URL')
|
||||
return
|
||||
}
|
||||
|
||||
setIsSwitching(true)
|
||||
try {
|
||||
await switchToFlask(flaskUrl)
|
||||
toast.success('Switched to Flask backend')
|
||||
} catch (error) {
|
||||
toast.error(`Failed to switch to Flask: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
} finally {
|
||||
setIsSwitching(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSwitchToSQLite = async () => {
|
||||
if (backend === 'sqlite') {
|
||||
toast.info('Already using SQLite')
|
||||
return
|
||||
}
|
||||
|
||||
setIsSwitching(true)
|
||||
try {
|
||||
await switchToSQLite()
|
||||
toast.success('Switched to SQLite storage')
|
||||
} catch (error) {
|
||||
toast.error(`Failed to switch to SQLite: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
} finally {
|
||||
setIsSwitching(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSwitchToIndexedDB = async () => {
|
||||
if (backend === 'indexeddb') {
|
||||
toast.info('Already using IndexedDB')
|
||||
return
|
||||
}
|
||||
|
||||
setIsSwitching(true)
|
||||
try {
|
||||
await switchToIndexedDB()
|
||||
toast.success('Switched to IndexedDB storage (default)')
|
||||
} catch (error) {
|
||||
toast.error(`Failed to switch to IndexedDB: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
} finally {
|
||||
setIsSwitching(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
setIsExporting(true)
|
||||
try {
|
||||
const data = await exportData()
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `codeforge-data-${new Date().toISOString().split('T')[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
toast.success('Data exported successfully')
|
||||
} catch (error) {
|
||||
toast.error(`Failed to export data: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
} finally {
|
||||
setIsExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = async () => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'application/json'
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
|
||||
setIsImporting(true)
|
||||
try {
|
||||
const text = await file.text()
|
||||
const data = JSON.parse(text)
|
||||
await importData(data)
|
||||
toast.success('Data imported successfully')
|
||||
} catch (error) {
|
||||
toast.error(`Failed to import data: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
} finally {
|
||||
setIsImporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
input.click()
|
||||
}
|
||||
|
||||
const getBackendIcon = () => {
|
||||
switch (backend) {
|
||||
case 'flask':
|
||||
return <CloudArrowUp className="w-5 h-5" />
|
||||
case 'sqlite':
|
||||
return <HardDrive className="w-5 h-5" />
|
||||
case 'indexeddb':
|
||||
return <Database className="w-5 h-5" />
|
||||
case 'sparkkv':
|
||||
return <Cloud className="w-5 h-5" />
|
||||
default:
|
||||
return <Database className="w-5 h-5" />
|
||||
}
|
||||
}
|
||||
|
||||
const getBackendLabel = () => {
|
||||
switch (backend) {
|
||||
case 'flask':
|
||||
return 'Flask Backend (Remote)'
|
||||
case 'sqlite':
|
||||
return 'SQLite (On-disk)'
|
||||
case 'indexeddb':
|
||||
return 'IndexedDB (Browser) - Default'
|
||||
case 'sparkkv':
|
||||
return 'Spark KV (Cloud)'
|
||||
default:
|
||||
return 'Unknown'
|
||||
}
|
||||
}
|
||||
|
||||
const getBackendDescription = () => {
|
||||
switch (backend) {
|
||||
case 'flask':
|
||||
return 'Data stored on Flask server with SQLite (cross-device sync)'
|
||||
case 'sqlite':
|
||||
return 'Data stored in SQLite database persisted to localStorage'
|
||||
case 'indexeddb':
|
||||
return 'Data stored in browser IndexedDB (default, recommended for most users)'
|
||||
case 'sparkkv':
|
||||
return 'Data stored in Spark cloud key-value store'
|
||||
default:
|
||||
return 'No storage backend detected'
|
||||
}
|
||||
}
|
||||
const { isExporting, isImporting, handleExport, handleImport } = useStorageDataHandlers({
|
||||
exportData,
|
||||
importData,
|
||||
exportFilename: () => {
|
||||
const dateStamp = new Date().toISOString().split('T')[0]
|
||||
return `${storageSettingsCopy.panel.exportFilenamePrefix}-${dateStamp}.json`
|
||||
},
|
||||
importAccept: 'application/json',
|
||||
})
|
||||
|
||||
if (isLoading && !backend) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="w-5 h-5" />
|
||||
Storage Settings
|
||||
</CardTitle>
|
||||
<CardDescription>Detecting storage backend...</CardDescription>
|
||||
</CardHeader>
|
||||
<StorageSettingsPanelHeader description={storageSettingsCopy.panel.loadingDescription} />
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<CircleNotch className="w-8 h-8 animate-spin text-muted-foreground" />
|
||||
@@ -192,128 +231,24 @@ export function StorageSettingsPanel() {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="w-5 h-5" />
|
||||
Storage Settings
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your local data storage preferences
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<StorageSettingsPanelHeader description={storageSettingsCopy.panel.description} />
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">Current Backend:</span>
|
||||
{getBackendIcon()}
|
||||
<span className="text-sm">{getBackendLabel()}</span>
|
||||
</div>
|
||||
<Badge variant="secondary">{backend?.toUpperCase() || 'UNKNOWN'}</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{getBackendDescription()}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium">Switch Storage Backend</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="flask-url" className="text-sm">Flask Backend URL (Optional)</Label>
|
||||
<Input
|
||||
id="flask-url"
|
||||
type="text"
|
||||
value={flaskUrl}
|
||||
onChange={(e) => setFlaskUrl(e.target.value)}
|
||||
placeholder="http://localhost:5001"
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Enter Flask backend URL or set VITE_FLASK_BACKEND_URL environment variable
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={handleSwitchToIndexedDB}
|
||||
disabled={backend === 'indexeddb' || isSwitching}
|
||||
variant={backend === 'indexeddb' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
>
|
||||
{isSwitching ? (
|
||||
<CircleNotch className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Database className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
IndexedDB (Default)
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSwitchToFlask}
|
||||
disabled={backend === 'flask' || isSwitching}
|
||||
variant={backend === 'flask' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
>
|
||||
{isSwitching ? (
|
||||
<CircleNotch className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<CloudArrowUp className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Flask Backend
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSwitchToSQLite}
|
||||
disabled={backend === 'sqlite' || isSwitching}
|
||||
variant={backend === 'sqlite' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
>
|
||||
{isSwitching ? (
|
||||
<CircleNotch className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<HardDrive className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
SQLite
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
IndexedDB is the default and works offline. Flask backend enables cross-device sync.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium">Data Management</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
disabled={isExporting}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
{isExporting ? (
|
||||
<CircleNotch className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Export Data
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
disabled={isImporting}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
{isImporting ? (
|
||||
<CircleNotch className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Import Data
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Export your data as a JSON file or import from a previous backup
|
||||
</p>
|
||||
</div>
|
||||
<BackendStatusSummary backend={backend} />
|
||||
<BackendSwitcher
|
||||
backend={backend}
|
||||
flaskUrl={flaskUrl}
|
||||
isSwitching={isSwitching}
|
||||
onFlaskUrlChange={setFlaskUrl}
|
||||
onSwitchToIndexedDB={handleSwitchToIndexedDB}
|
||||
onSwitchToFlask={handleSwitchToFlask}
|
||||
onSwitchToSQLite={handleSwitchToSQLite}
|
||||
/>
|
||||
<DataManagementSection
|
||||
isExporting={isExporting}
|
||||
isImporting={isImporting}
|
||||
onExport={handleExport}
|
||||
onImport={handleImport}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -5,8 +5,165 @@ import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useStorageBackend } from '@/hooks/use-unified-storage'
|
||||
import { toast } from 'sonner'
|
||||
import { Database, HardDrive, Cloud, Cpu, Download, Upload } from '@phosphor-icons/react'
|
||||
import {
|
||||
storageSettingsCopy,
|
||||
getBackendCopy,
|
||||
type StorageBackendKey,
|
||||
} from '@/components/storage/storageSettingsConfig'
|
||||
import { useStorageSwitchHandlers } from '@/components/storage/useStorageSwitchHandlers'
|
||||
import { useStorageDataHandlers } from '@/components/storage/useStorageDataHandlers'
|
||||
|
||||
const getBackendIcon = (backend: StorageBackendKey | null) => {
|
||||
switch (backend) {
|
||||
case 'flask':
|
||||
return <Cpu className="w-5 h-5" />
|
||||
case 'indexeddb':
|
||||
return <HardDrive className="w-5 h-5" />
|
||||
case 'sqlite':
|
||||
return <Database className="w-5 h-5" />
|
||||
case 'sparkkv':
|
||||
return <Cloud className="w-5 h-5" />
|
||||
default:
|
||||
return <Database className="w-5 h-5" />
|
||||
}
|
||||
}
|
||||
|
||||
type BackendCardProps = {
|
||||
backend: StorageBackendKey | null
|
||||
isLoading: boolean
|
||||
flaskUrl: string
|
||||
isSwitching: boolean
|
||||
onFlaskUrlChange: (value: string) => void
|
||||
onSwitchToFlask: () => void
|
||||
onSwitchToIndexedDB: () => void
|
||||
onSwitchToSQLite: () => void
|
||||
}
|
||||
|
||||
const BackendCard = ({
|
||||
backend,
|
||||
isLoading,
|
||||
flaskUrl,
|
||||
isSwitching,
|
||||
onFlaskUrlChange,
|
||||
onSwitchToFlask,
|
||||
onSwitchToIndexedDB,
|
||||
onSwitchToSQLite,
|
||||
}: BackendCardProps) => {
|
||||
const backendCopy = getBackendCopy(backend)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
{getBackendIcon(backend)}
|
||||
{storageSettingsCopy.molecule.title}
|
||||
</CardTitle>
|
||||
<CardDescription>{storageSettingsCopy.molecule.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">{storageSettingsCopy.molecule.currentBackendLabel}</span>
|
||||
<Badge variant="secondary" className="flex items-center gap-1">
|
||||
{getBackendIcon(backend)}
|
||||
{backendCopy.moleculeLabel}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="flask-url">{storageSettingsCopy.molecule.flaskUrlLabel}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="flask-url"
|
||||
value={flaskUrl}
|
||||
onChange={(e) => onFlaskUrlChange(e.target.value)}
|
||||
placeholder={storageSettingsCopy.molecule.flaskUrlPlaceholder}
|
||||
disabled={isSwitching || isLoading}
|
||||
/>
|
||||
<Button
|
||||
onClick={onSwitchToFlask}
|
||||
disabled={isSwitching || isLoading || backend === 'flask'}
|
||||
variant={backend === 'flask' ? 'secondary' : 'default'}
|
||||
>
|
||||
<Cpu className="w-4 h-4 mr-2" />
|
||||
{backend === 'flask'
|
||||
? storageSettingsCopy.molecule.buttons.flaskActive
|
||||
: storageSettingsCopy.molecule.buttons.flaskUse}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{storageSettingsCopy.molecule.flaskHelp}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={onSwitchToIndexedDB}
|
||||
disabled={isSwitching || isLoading || backend === 'indexeddb'}
|
||||
variant={backend === 'indexeddb' ? 'secondary' : 'outline'}
|
||||
className="flex-1"
|
||||
>
|
||||
<HardDrive className="w-4 h-4 mr-2" />
|
||||
{backend === 'indexeddb'
|
||||
? storageSettingsCopy.molecule.buttons.indexeddbActive
|
||||
: storageSettingsCopy.molecule.buttons.indexeddbUse}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onSwitchToSQLite}
|
||||
disabled={isSwitching || isLoading || backend === 'sqlite'}
|
||||
variant={backend === 'sqlite' ? 'secondary' : 'outline'}
|
||||
className="flex-1"
|
||||
>
|
||||
<Database className="w-4 h-4 mr-2" />
|
||||
{backend === 'sqlite'
|
||||
? storageSettingsCopy.molecule.buttons.sqliteActive
|
||||
: storageSettingsCopy.molecule.buttons.sqliteUse}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<p>{storageSettingsCopy.molecule.backendDetails.indexeddb}</p>
|
||||
<p>{storageSettingsCopy.molecule.backendDetails.sqlite}</p>
|
||||
<p>{storageSettingsCopy.molecule.backendDetails.flask}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
type DataManagementCardProps = {
|
||||
isExporting: boolean
|
||||
isImporting: boolean
|
||||
onExport: () => void
|
||||
onImport: () => void
|
||||
}
|
||||
|
||||
const DataManagementCard = ({
|
||||
isExporting,
|
||||
isImporting,
|
||||
onExport,
|
||||
onImport,
|
||||
}: DataManagementCardProps) => (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{storageSettingsCopy.molecule.dataTitle}</CardTitle>
|
||||
<CardDescription>{storageSettingsCopy.molecule.dataDescription}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={onExport} variant="outline" className="flex-1" disabled={isExporting}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
{storageSettingsCopy.molecule.buttons.export}
|
||||
</Button>
|
||||
<Button onClick={onImport} variant="outline" className="flex-1" disabled={isImporting}>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
{storageSettingsCopy.molecule.buttons.import}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{storageSettingsCopy.molecule.dataHelp}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
export function StorageSettings() {
|
||||
const {
|
||||
@@ -20,214 +177,43 @@ export function StorageSettings() {
|
||||
} = useStorageBackend()
|
||||
|
||||
const [flaskUrl, setFlaskUrl] = useState(
|
||||
localStorage.getItem('codeforge-flask-url') || 'http://localhost:5001'
|
||||
localStorage.getItem('codeforge-flask-url') || storageSettingsCopy.molecule.flaskUrlPlaceholder
|
||||
)
|
||||
const [isSwitching, setIsSwitching] = useState(false)
|
||||
|
||||
const handleSwitchToFlask = async () => {
|
||||
setIsSwitching(true)
|
||||
try {
|
||||
await switchToFlask(flaskUrl)
|
||||
toast.success('Switched to Flask backend')
|
||||
} catch (error) {
|
||||
toast.error(`Failed to switch to Flask: ${error}`)
|
||||
} finally {
|
||||
setIsSwitching(false)
|
||||
}
|
||||
}
|
||||
const { isSwitching, handleSwitchToFlask, handleSwitchToSQLite, handleSwitchToIndexedDB } =
|
||||
useStorageSwitchHandlers({
|
||||
backend,
|
||||
flaskUrl,
|
||||
switchToFlask,
|
||||
switchToSQLite,
|
||||
switchToIndexedDB,
|
||||
})
|
||||
|
||||
const handleSwitchToIndexedDB = async () => {
|
||||
setIsSwitching(true)
|
||||
try {
|
||||
await switchToIndexedDB()
|
||||
toast.success('Switched to IndexedDB')
|
||||
} catch (error) {
|
||||
toast.error(`Failed to switch to IndexedDB: ${error}`)
|
||||
} finally {
|
||||
setIsSwitching(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSwitchToSQLite = async () => {
|
||||
setIsSwitching(true)
|
||||
try {
|
||||
await switchToSQLite()
|
||||
toast.success('Switched to SQLite')
|
||||
} catch (error) {
|
||||
toast.error(`Failed to switch to SQLite: ${error}`)
|
||||
} finally {
|
||||
setIsSwitching(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
const data = await exportData()
|
||||
const json = JSON.stringify(data, null, 2)
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `codeforge-backup-${Date.now()}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
toast.success('Data exported successfully')
|
||||
} catch (error) {
|
||||
toast.error(`Failed to export data: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = () => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = '.json'
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const text = await file.text()
|
||||
const data = JSON.parse(text)
|
||||
await importData(data)
|
||||
toast.success('Data imported successfully')
|
||||
} catch (error) {
|
||||
toast.error(`Failed to import data: ${error}`)
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
const getBackendIcon = (backendType: string | null) => {
|
||||
switch (backendType) {
|
||||
case 'flask':
|
||||
return <Cpu className="w-5 h-5" />
|
||||
case 'indexeddb':
|
||||
return <HardDrive className="w-5 h-5" />
|
||||
case 'sqlite':
|
||||
return <Database className="w-5 h-5" />
|
||||
case 'sparkkv':
|
||||
return <Cloud className="w-5 h-5" />
|
||||
default:
|
||||
return <Database className="w-5 h-5" />
|
||||
}
|
||||
}
|
||||
|
||||
const getBackendLabel = (backendType: string | null) => {
|
||||
switch (backendType) {
|
||||
case 'flask':
|
||||
return 'Flask Backend'
|
||||
case 'indexeddb':
|
||||
return 'IndexedDB'
|
||||
case 'sqlite':
|
||||
return 'SQLite'
|
||||
case 'sparkkv':
|
||||
return 'Spark KV'
|
||||
default:
|
||||
return 'Unknown'
|
||||
}
|
||||
}
|
||||
const { isExporting, isImporting, handleExport, handleImport } = useStorageDataHandlers({
|
||||
exportData,
|
||||
importData,
|
||||
exportFilename: () => `${storageSettingsCopy.molecule.exportFilenamePrefix}-${Date.now()}.json`,
|
||||
importAccept: '.json',
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
{getBackendIcon(backend)}
|
||||
Storage Backend
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Choose where your data is stored
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Current backend:</span>
|
||||
<Badge variant="secondary" className="flex items-center gap-1">
|
||||
{getBackendIcon(backend)}
|
||||
{getBackendLabel(backend)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="flask-url">Flask Backend URL</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="flask-url"
|
||||
value={flaskUrl}
|
||||
onChange={(e) => setFlaskUrl(e.target.value)}
|
||||
placeholder="http://localhost:5001"
|
||||
disabled={isSwitching || isLoading}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSwitchToFlask}
|
||||
disabled={isSwitching || isLoading || backend === 'flask'}
|
||||
variant={backend === 'flask' ? 'secondary' : 'default'}
|
||||
>
|
||||
<Cpu className="w-4 h-4 mr-2" />
|
||||
{backend === 'flask' ? 'Active' : 'Use Flask'}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Store data on a Flask server (persistent across devices)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleSwitchToIndexedDB}
|
||||
disabled={isSwitching || isLoading || backend === 'indexeddb'}
|
||||
variant={backend === 'indexeddb' ? 'secondary' : 'outline'}
|
||||
className="flex-1"
|
||||
>
|
||||
<HardDrive className="w-4 h-4 mr-2" />
|
||||
{backend === 'indexeddb' ? 'Active' : 'Use IndexedDB'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSwitchToSQLite}
|
||||
disabled={isSwitching || isLoading || backend === 'sqlite'}
|
||||
variant={backend === 'sqlite' ? 'secondary' : 'outline'}
|
||||
className="flex-1"
|
||||
>
|
||||
<Database className="w-4 h-4 mr-2" />
|
||||
{backend === 'sqlite' ? 'Active' : 'Use SQLite'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<p><strong>IndexedDB (Default):</strong> Browser storage, large capacity, works offline</p>
|
||||
<p><strong>SQLite:</strong> Browser storage with SQL queries, requires sql.js package</p>
|
||||
<p><strong>Flask:</strong> Server storage, persistent across devices, requires backend</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Data Management</CardTitle>
|
||||
<CardDescription>
|
||||
Export or import your data
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleExport} variant="outline" className="flex-1">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Export Data
|
||||
</Button>
|
||||
<Button onClick={handleImport} variant="outline" className="flex-1">
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
Import Data
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Backup your data to a JSON file or restore from a previous backup
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<BackendCard
|
||||
backend={backend}
|
||||
isLoading={isLoading}
|
||||
flaskUrl={flaskUrl}
|
||||
isSwitching={isSwitching}
|
||||
onFlaskUrlChange={setFlaskUrl}
|
||||
onSwitchToFlask={handleSwitchToFlask}
|
||||
onSwitchToIndexedDB={handleSwitchToIndexedDB}
|
||||
onSwitchToSQLite={handleSwitchToSQLite}
|
||||
/>
|
||||
<DataManagementCard
|
||||
isExporting={isExporting}
|
||||
isImporting={isImporting}
|
||||
onExport={handleExport}
|
||||
onImport={handleImport}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
13
src/components/storage/storageSettingsConfig.ts
Normal file
13
src/components/storage/storageSettingsConfig.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import copy from './storageSettingsCopy.json'
|
||||
|
||||
export type StorageBackendKey = 'flask' | 'sqlite' | 'indexeddb' | 'sparkkv'
|
||||
|
||||
export const storageSettingsCopy = copy
|
||||
|
||||
export const getBackendCopy = (backend: StorageBackendKey | null) => {
|
||||
if (!backend) {
|
||||
return storageSettingsCopy.backends.unknown
|
||||
}
|
||||
|
||||
return storageSettingsCopy.backends[backend] ?? storageSettingsCopy.backends.unknown
|
||||
}
|
||||
106
src/components/storage/storageSettingsCopy.json
Normal file
106
src/components/storage/storageSettingsCopy.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"panel": {
|
||||
"title": "Storage Settings",
|
||||
"description": "Manage your local data storage preferences",
|
||||
"loadingDescription": "Detecting storage backend...",
|
||||
"currentBackendLabel": "Current Backend:",
|
||||
"switchTitle": "Switch Storage Backend",
|
||||
"flaskUrlLabel": "Flask Backend URL (Optional)",
|
||||
"flaskUrlPlaceholder": "http://localhost:5001",
|
||||
"flaskUrlHelp": "Enter Flask backend URL or set VITE_FLASK_BACKEND_URL environment variable",
|
||||
"switchHelp": "IndexedDB is the default and works offline. Flask backend enables cross-device sync.",
|
||||
"dataTitle": "Data Management",
|
||||
"dataHelp": "Export your data as a JSON file or import from a previous backup",
|
||||
"buttons": {
|
||||
"indexeddb": "IndexedDB (Default)",
|
||||
"flask": "Flask Backend",
|
||||
"sqlite": "SQLite",
|
||||
"export": "Export Data",
|
||||
"import": "Import Data"
|
||||
},
|
||||
"exportFilenamePrefix": "codeforge-data"
|
||||
},
|
||||
"molecule": {
|
||||
"title": "Storage Backend",
|
||||
"description": "Choose where your data is stored",
|
||||
"currentBackendLabel": "Current backend:",
|
||||
"flaskUrlLabel": "Flask Backend URL",
|
||||
"flaskUrlPlaceholder": "http://localhost:5001",
|
||||
"flaskHelp": "Store data on a Flask server (persistent across devices)",
|
||||
"dataTitle": "Data Management",
|
||||
"dataDescription": "Export or import your data",
|
||||
"dataHelp": "Backup your data to a JSON file or restore from a previous backup",
|
||||
"buttons": {
|
||||
"flaskActive": "Active",
|
||||
"flaskUse": "Use Flask",
|
||||
"indexeddbActive": "Active",
|
||||
"indexeddbUse": "Use IndexedDB",
|
||||
"sqliteActive": "Active",
|
||||
"sqliteUse": "Use SQLite",
|
||||
"export": "Export Data",
|
||||
"import": "Import Data"
|
||||
},
|
||||
"backendDetails": {
|
||||
"indexeddb": "IndexedDB (Default): Browser storage, large capacity, works offline",
|
||||
"sqlite": "SQLite: Browser storage with SQL queries, requires sql.js package",
|
||||
"flask": "Flask: Server storage, persistent across devices, requires backend"
|
||||
},
|
||||
"exportFilenamePrefix": "codeforge-backup"
|
||||
},
|
||||
"backends": {
|
||||
"flask": {
|
||||
"panelLabel": "Flask Backend (Remote)",
|
||||
"panelDescription": "Data stored on Flask server with SQLite (cross-device sync)",
|
||||
"moleculeLabel": "Flask Backend",
|
||||
"badgeLabel": "Flask"
|
||||
},
|
||||
"sqlite": {
|
||||
"panelLabel": "SQLite (On-disk)",
|
||||
"panelDescription": "Data stored in SQLite database persisted to localStorage",
|
||||
"moleculeLabel": "SQLite",
|
||||
"badgeLabel": "SQLite"
|
||||
},
|
||||
"indexeddb": {
|
||||
"panelLabel": "IndexedDB (Browser) - Default",
|
||||
"panelDescription": "Data stored in browser IndexedDB (default, recommended for most users)",
|
||||
"moleculeLabel": "IndexedDB",
|
||||
"badgeLabel": "IndexedDB"
|
||||
},
|
||||
"sparkkv": {
|
||||
"panelLabel": "Spark KV (Cloud)",
|
||||
"panelDescription": "Data stored in Spark cloud key-value store",
|
||||
"moleculeLabel": "Spark KV",
|
||||
"badgeLabel": "Spark KV"
|
||||
},
|
||||
"unknown": {
|
||||
"panelLabel": "Unknown",
|
||||
"panelDescription": "No storage backend detected",
|
||||
"moleculeLabel": "Unknown",
|
||||
"badgeLabel": "Unknown"
|
||||
}
|
||||
},
|
||||
"toasts": {
|
||||
"alreadyUsing": {
|
||||
"flask": "Already using Flask backend",
|
||||
"sqlite": "Already using SQLite",
|
||||
"indexeddb": "Already using IndexedDB"
|
||||
},
|
||||
"errors": {
|
||||
"missingFlaskUrl": "Please enter a Flask backend URL"
|
||||
},
|
||||
"success": {
|
||||
"switchFlask": "Switched to Flask backend",
|
||||
"switchSQLite": "Switched to SQLite storage",
|
||||
"switchIndexedDB": "Switched to IndexedDB storage (default)",
|
||||
"export": "Data exported successfully",
|
||||
"import": "Data imported successfully"
|
||||
},
|
||||
"failure": {
|
||||
"switchFlask": "Failed to switch to Flask",
|
||||
"switchSQLite": "Failed to switch to SQLite",
|
||||
"switchIndexedDB": "Failed to switch to IndexedDB",
|
||||
"export": "Failed to export data",
|
||||
"import": "Failed to import data"
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/components/storage/storageSettingsUtils.ts
Normal file
34
src/components/storage/storageSettingsUtils.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export const formatStorageError = (error: unknown) => {
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
return String(error)
|
||||
}
|
||||
|
||||
export const downloadJson = (data: unknown, filename: string) => {
|
||||
const json = JSON.stringify(data, null, 2)
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const anchor = document.createElement('a')
|
||||
|
||||
anchor.href = url
|
||||
anchor.download = filename
|
||||
document.body.appendChild(anchor)
|
||||
anchor.click()
|
||||
document.body.removeChild(anchor)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
export const createJsonFileInput = (accept: string, onFileLoaded: (file: File) => void) => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = accept
|
||||
input.onchange = (event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
if (file) {
|
||||
onFileLoaded(file)
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
57
src/components/storage/useStorageDataHandlers.ts
Normal file
57
src/components/storage/useStorageDataHandlers.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { createJsonFileInput, downloadJson, formatStorageError } from './storageSettingsUtils'
|
||||
import { storageSettingsCopy } from './storageSettingsConfig'
|
||||
|
||||
type DataHandlers = {
|
||||
exportData: () => Promise<unknown>
|
||||
importData: (data: unknown) => Promise<void>
|
||||
exportFilename: () => string
|
||||
importAccept: string
|
||||
}
|
||||
|
||||
export const useStorageDataHandlers = ({
|
||||
exportData,
|
||||
importData,
|
||||
exportFilename,
|
||||
importAccept,
|
||||
}: DataHandlers) => {
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
const [isImporting, setIsImporting] = useState(false)
|
||||
|
||||
const handleExport = useCallback(async () => {
|
||||
setIsExporting(true)
|
||||
try {
|
||||
const data = await exportData()
|
||||
downloadJson(data, exportFilename())
|
||||
toast.success(storageSettingsCopy.toasts.success.export)
|
||||
} catch (error) {
|
||||
toast.error(`${storageSettingsCopy.toasts.failure.export}: ${formatStorageError(error)}`)
|
||||
} finally {
|
||||
setIsExporting(false)
|
||||
}
|
||||
}, [exportData, exportFilename])
|
||||
|
||||
const handleImport = useCallback(() => {
|
||||
createJsonFileInput(importAccept, async (file) => {
|
||||
setIsImporting(true)
|
||||
try {
|
||||
const text = await file.text()
|
||||
const data = JSON.parse(text)
|
||||
await importData(data)
|
||||
toast.success(storageSettingsCopy.toasts.success.import)
|
||||
} catch (error) {
|
||||
toast.error(`${storageSettingsCopy.toasts.failure.import}: ${formatStorageError(error)}`)
|
||||
} finally {
|
||||
setIsImporting(false)
|
||||
}
|
||||
})
|
||||
}, [importAccept, importData])
|
||||
|
||||
return {
|
||||
isExporting,
|
||||
isImporting,
|
||||
handleExport,
|
||||
handleImport,
|
||||
}
|
||||
}
|
||||
85
src/components/storage/useStorageSwitchHandlers.ts
Normal file
85
src/components/storage/useStorageSwitchHandlers.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { formatStorageError } from './storageSettingsUtils'
|
||||
import { storageSettingsCopy, type StorageBackendKey } from './storageSettingsConfig'
|
||||
|
||||
type SwitchHandlers = {
|
||||
backend: StorageBackendKey | null
|
||||
flaskUrl: string
|
||||
switchToFlask: (url: string) => Promise<void>
|
||||
switchToSQLite: () => Promise<void>
|
||||
switchToIndexedDB: () => Promise<void>
|
||||
}
|
||||
|
||||
export const useStorageSwitchHandlers = ({
|
||||
backend,
|
||||
flaskUrl,
|
||||
switchToFlask,
|
||||
switchToSQLite,
|
||||
switchToIndexedDB,
|
||||
}: SwitchHandlers) => {
|
||||
const [isSwitching, setIsSwitching] = useState(false)
|
||||
|
||||
const handleSwitchToFlask = useCallback(async () => {
|
||||
if (backend === 'flask') {
|
||||
toast.info(storageSettingsCopy.toasts.alreadyUsing.flask)
|
||||
return
|
||||
}
|
||||
|
||||
if (!flaskUrl) {
|
||||
toast.error(storageSettingsCopy.toasts.errors.missingFlaskUrl)
|
||||
return
|
||||
}
|
||||
|
||||
setIsSwitching(true)
|
||||
try {
|
||||
await switchToFlask(flaskUrl)
|
||||
toast.success(storageSettingsCopy.toasts.success.switchFlask)
|
||||
} catch (error) {
|
||||
toast.error(`${storageSettingsCopy.toasts.failure.switchFlask}: ${formatStorageError(error)}`)
|
||||
} finally {
|
||||
setIsSwitching(false)
|
||||
}
|
||||
}, [backend, flaskUrl, switchToFlask])
|
||||
|
||||
const handleSwitchToSQLite = useCallback(async () => {
|
||||
if (backend === 'sqlite') {
|
||||
toast.info(storageSettingsCopy.toasts.alreadyUsing.sqlite)
|
||||
return
|
||||
}
|
||||
|
||||
setIsSwitching(true)
|
||||
try {
|
||||
await switchToSQLite()
|
||||
toast.success(storageSettingsCopy.toasts.success.switchSQLite)
|
||||
} catch (error) {
|
||||
toast.error(`${storageSettingsCopy.toasts.failure.switchSQLite}: ${formatStorageError(error)}`)
|
||||
} finally {
|
||||
setIsSwitching(false)
|
||||
}
|
||||
}, [backend, switchToSQLite])
|
||||
|
||||
const handleSwitchToIndexedDB = useCallback(async () => {
|
||||
if (backend === 'indexeddb') {
|
||||
toast.info(storageSettingsCopy.toasts.alreadyUsing.indexeddb)
|
||||
return
|
||||
}
|
||||
|
||||
setIsSwitching(true)
|
||||
try {
|
||||
await switchToIndexedDB()
|
||||
toast.success(storageSettingsCopy.toasts.success.switchIndexedDB)
|
||||
} catch (error) {
|
||||
toast.error(`${storageSettingsCopy.toasts.failure.switchIndexedDB}: ${formatStorageError(error)}`)
|
||||
} finally {
|
||||
setIsSwitching(false)
|
||||
}
|
||||
}, [backend, switchToIndexedDB])
|
||||
|
||||
return {
|
||||
isSwitching,
|
||||
handleSwitchToFlask,
|
||||
handleSwitchToSQLite,
|
||||
handleSwitchToIndexedDB,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user