- 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.
177 lines
7.4 KiB
TypeScript
177 lines
7.4 KiB
TypeScript
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>
|
|
);
|
|
}
|