feat(account): danger zone, restore import, larger API key field on phones
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m27s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m27s
- Danger zone in settings: delete all own data or the account, both with password confirmation; the last server admin cannot delete the account - DELETE /user/data and /user/account; PurgeUserData removes everything a user owns in one transaction (others' tasks in shared lists stay theirs), also used by admin user deletion - MindSift import: 'Hinzufügen' or 'Wiederherstellen' (mode=restore, wipes own data only after the file is fully validated, confirm checkbox) - API key name field and button 44 px on phones (flex-1 collapsed them) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9bc8716f41
commit
0e9fd1b3ad
11 files changed
+472
-15
No files matched your search
@@ -30,11 +30,16 @@ export const ImportCard: React.FC<ImportCardProps> = ({ source, title, descripti
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
// MindSift data can replace the current state instead of being added next to it
|
||||
const [mode, setMode] = useState<'merge' | 'restore'>('merge');
|
||||
const [confirmRestore, setConfirmRestore] = useState(false);
|
||||
const restoring = source === 'mindsift' && mode === 'restore';
|
||||
|
||||
const reset = () => {
|
||||
setFile(null);
|
||||
setPreview(null);
|
||||
setError(null);
|
||||
setConfirmRestore(false);
|
||||
};
|
||||
|
||||
const pick = async (f?: File) => {
|
||||
@@ -57,9 +62,10 @@ export const ImportCard: React.FC<ImportCardProps> = ({ source, title, descripti
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
setResult(await api.importData(source, file, false));
|
||||
setResult(await api.importData(source, file, false, restoring ? 'restore' : 'merge'));
|
||||
setFile(null);
|
||||
setPreview(null);
|
||||
setConfirmRestore(false);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Import fehlgeschlagen');
|
||||
} finally {
|
||||
@@ -76,6 +82,38 @@ export const ImportCard: React.FC<ImportCardProps> = ({ source, title, descripti
|
||||
<p className="text-muted text-[11px] mt-0.5">{description}</p>
|
||||
</div>
|
||||
|
||||
{source === 'mindsift' && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2" role="radiogroup" aria-label="Import-Modus">
|
||||
{(
|
||||
[
|
||||
['merge', 'Hinzufügen', 'Bestehende Daten behalten, Backup daneben anlegen.'],
|
||||
['restore', 'Wiederherstellen', 'Eigene Notizen, Aufgaben und Listen vorher löschen.'],
|
||||
] as const
|
||||
).map(([value, label, text]) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={mode === value}
|
||||
onClick={() => {
|
||||
setMode(value);
|
||||
setConfirmRestore(false);
|
||||
}}
|
||||
className={`p-2.5 min-h-[44px] text-left border rounded-card transition-colors ${
|
||||
mode === value
|
||||
? value === 'restore'
|
||||
? 'border-status-err bg-status-err/10'
|
||||
: 'border-accent bg-accent/10'
|
||||
: 'border-border hover:border-strong'
|
||||
}`}
|
||||
>
|
||||
<span className={`block text-xs font-semibold ${value === 'restore' && mode === value ? 'text-status-err' : 'text-main'}`}>{label}</span>
|
||||
<span className="block text-[11px] text-muted">{text}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -147,10 +185,30 @@ export const ImportCard: React.FC<ImportCardProps> = ({ source, title, descripti
|
||||
</ul>
|
||||
</details>
|
||||
)}
|
||||
{!result && restoring && (
|
||||
<label className="flex items-start gap-2 p-2.5 rounded-card border border-status-err/40 bg-status-err/10 text-[11px] text-main cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={confirmRestore}
|
||||
onChange={(e) => setConfirmRestore(e.target.checked)}
|
||||
className="mt-0.5 w-4 h-4 shrink-0"
|
||||
/>
|
||||
<span>
|
||||
<strong className="text-status-err">Achtung:</strong> Alle deine bisherigen Notizen, Ordner, Aufgaben, Gedanken und
|
||||
Listen werden vor dem Import unwiderruflich gelöscht und durch das Backup ersetzt. Mit dir geteilte Inhalte anderer
|
||||
bleiben erhalten.
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
{!result && (
|
||||
<div className="flex flex-col sm:flex-row gap-2 pt-1">
|
||||
<button type="button" onClick={runImport} className="btn-primary h-11 sm:h-8 px-4 text-xs font-semibold">
|
||||
Jetzt importieren
|
||||
<button
|
||||
type="button"
|
||||
onClick={runImport}
|
||||
disabled={restoring && !confirmRestore}
|
||||
className={`h-11 sm:h-8 px-4 text-xs font-semibold disabled:opacity-50 ${restoring ? 'btn-action border-status-err text-status-err' : 'btn-primary'}`}
|
||||
>
|
||||
{restoring ? 'Löschen und wiederherstellen' : 'Jetzt importieren'}
|
||||
</button>
|
||||
<button type="button" onClick={reset} className="btn-action h-11 sm:h-8 px-3 text-xs">
|
||||
Abbrechen
|
||||
|
||||
Reference in new issue
Block a user