Add foundational components and logic for migration, UI design, and input handling
- Introduced foundational UI components (`Badge`, `LockCard`, `SectionHeader`, `AvatarIcon`, etc.) for flexible layouts and consistent design. - Added migration support with the `MigrationModal` component and backend integration for exporting/importing data between Electron and Tauri. - Extended form components with `TextAreaInput`, `OrderInput`, `ToggleField`, and `ToolbarSelect` for improved input handling. - Updated `ScribeShell` with migration popup logic to prompt users for data migration. - Integrated `AlertStack` for better alert handling and notification management. - Enhanced Rust/Tauri services with migration command implementations. - Added translations and styles for new components.
This commit is contained in:
176
components/migration/MigrationModal.tsx
Normal file
176
components/migration/MigrationModal.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import React, {useState} from 'react';
|
||||
import {ArrowRightLeft, CheckCircle, AlertTriangle, FolderOpen, Loader2} from 'lucide-react';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import Button from '@/components/ui/Button';
|
||||
import {useTranslations} from '@/lib/i18n';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface MigrationModalProps {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
type MigrationStep = 'intro' | 'select' | 'importing' | 'success' | 'error';
|
||||
|
||||
export default function MigrationModal({onClose, onSuccess}: MigrationModalProps) {
|
||||
const t = useTranslations();
|
||||
const [step, setStep] = useState<MigrationStep>('intro');
|
||||
const [filePath, setFilePath] = useState<string>('');
|
||||
const [errorMsg, setErrorMsg] = useState<string>('');
|
||||
const [migratedUserId, setMigratedUserId] = useState<string>('');
|
||||
|
||||
async function handleCheck(): Promise<void> {
|
||||
if (!filePath.trim()) return;
|
||||
|
||||
try {
|
||||
const result: tauri.MigrationCheckResult = await tauri.checkElectronMigration(filePath.trim());
|
||||
if (!result.found) {
|
||||
setErrorMsg(t('migration.fileNotFound'));
|
||||
setStep('error');
|
||||
return;
|
||||
}
|
||||
if (!result.hasDb) {
|
||||
setErrorMsg(t('migration.dbNotFound'));
|
||||
setStep('error');
|
||||
return;
|
||||
}
|
||||
await handleImport();
|
||||
} catch (e: unknown) {
|
||||
setErrorMsg(e instanceof Error ? e.message : String(e));
|
||||
setStep('error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImport(): Promise<void> {
|
||||
setStep('importing');
|
||||
try {
|
||||
const result: tauri.MigrationResult = await tauri.importFromElectron(filePath.trim());
|
||||
if (result.success) {
|
||||
setMigratedUserId(result.userId || '');
|
||||
setStep('success');
|
||||
} else {
|
||||
setErrorMsg(result.error || t('migration.importFailed'));
|
||||
setStep('error');
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
setErrorMsg(e instanceof Error ? e.message : String(e));
|
||||
setStep('error');
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccessClose(): void {
|
||||
localStorage.setItem('electron_migration_done', 'true');
|
||||
onSuccess();
|
||||
}
|
||||
|
||||
function handleDismiss(): void {
|
||||
localStorage.setItem('electron_migration_dismissed', 'true');
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('migration.title')}
|
||||
icon={ArrowRightLeft}
|
||||
size="md"
|
||||
onClose={onClose}
|
||||
>
|
||||
{step === 'intro' && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-muted leading-relaxed">{t('migration.introText')}</p>
|
||||
<div className="bg-tertiary rounded-xl p-4 space-y-2">
|
||||
<p className="text-sm font-semibold text-text-primary">{t('migration.steps')}</p>
|
||||
<ol className="list-decimal list-inside text-sm text-muted space-y-1">
|
||||
<li>{t('migration.step1')}</li>
|
||||
<li>{t('migration.step2')}</li>
|
||||
<li>{t('migration.step3')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<Button variant="secondary" onClick={handleDismiss}>
|
||||
{t('migration.later')}
|
||||
</Button>
|
||||
<Button variant="primary" icon={FolderOpen} onClick={function (): void {
|
||||
setStep('select');
|
||||
}}>
|
||||
{t('migration.haveFile')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'select' && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-muted text-sm">{t('migration.selectText')}</p>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-text-primary">
|
||||
{t('migration.filePath')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={filePath}
|
||||
onChange={function (e: React.ChangeEvent<HTMLInputElement>): void {
|
||||
setFilePath(e.target.value);
|
||||
}}
|
||||
placeholder="/Users/.../Desktop/eritors-migration.json"
|
||||
className="w-full px-4 py-2.5 rounded-xl bg-tertiary border border-secondary text-text-primary text-sm placeholder:text-muted/50 focus:outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<Button variant="secondary" onClick={function (): void {
|
||||
setStep('intro');
|
||||
}}>
|
||||
{t('migration.back')}
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleCheck} disabled={!filePath.trim()}>
|
||||
{t('migration.import')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'importing' && (
|
||||
<div className="flex flex-col items-center py-8 space-y-4">
|
||||
<Loader2 className="w-10 h-10 text-primary animate-spin" strokeWidth={1.75}/>
|
||||
<p className="text-muted">{t('migration.importing')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'success' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col items-center py-6 space-y-3">
|
||||
<CheckCircle className="w-12 h-12 text-success" strokeWidth={1.75}/>
|
||||
<p className="text-text-primary font-semibold text-lg">{t('migration.successTitle')}</p>
|
||||
<p className="text-muted text-sm text-center">{t('migration.successText')}</p>
|
||||
</div>
|
||||
<div className="flex justify-end pt-2">
|
||||
<Button variant="primary" onClick={handleSuccessClose}>
|
||||
{t('migration.done')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'error' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col items-center py-6 space-y-3">
|
||||
<AlertTriangle className="w-12 h-12 text-error" strokeWidth={1.75}/>
|
||||
<p className="text-text-primary font-semibold">{t('migration.errorTitle')}</p>
|
||||
<p className="text-muted text-sm text-center">{errorMsg}</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
{t('migration.close')}
|
||||
</Button>
|
||||
<Button variant="primary" onClick={function (): void {
|
||||
setStep('select');
|
||||
setErrorMsg('');
|
||||
}}>
|
||||
{t('migration.retry')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user