-
-
Notifications
You must be signed in to change notification settings - Fork 446
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat-preferences: data backup export and import (#646)
* feat: data backups initial implementation * feat: improve data backups design in preferences * feat: split import backup in multiple segments * feat(preferences): move import backup spinner next to import button * fix(data-backups): padding between radio btn and buttons
- Loading branch information
Showing
3 changed files
with
184 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
181 changes: 181 additions & 0 deletions
181
app/assets/javascripts/preferences/panes/security-segments/DataBackups.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
import { isDesktopApplication } from '@/utils'; | ||
import { alertDialog } from '@Services/alertService'; | ||
import { | ||
STRING_IMPORT_SUCCESS, | ||
STRING_INVALID_IMPORT_FILE, | ||
STRING_UNSUPPORTED_BACKUP_FILE_VERSION, | ||
StringImportError | ||
} from '@/strings'; | ||
import { BackupFile } from '@standardnotes/snjs'; | ||
import { useRef, useState } from 'preact/hooks'; | ||
import { WebApplication } from '@/ui_models/application'; | ||
import { JSXInternal } from 'preact/src/jsx'; | ||
import TargetedEvent = JSXInternal.TargetedEvent; | ||
import { AppState } from '@/ui_models/app_state'; | ||
import { observer } from 'mobx-react-lite'; | ||
import { PreferencesGroup, PreferencesSegment, Title, Text, Subtitle } from '../../components'; | ||
import { Button } from '@/components/Button'; | ||
|
||
type Props = { | ||
application: WebApplication; | ||
appState: AppState; | ||
} | ||
|
||
export const DataBackups = observer(({ | ||
application, | ||
appState | ||
}: Props) => { | ||
|
||
const fileInputRef = useRef<HTMLInputElement | null>(null); | ||
const [isImportDataLoading, setIsImportDataLoading] = useState(false); | ||
|
||
const { isBackupEncrypted, isEncryptionEnabled, setIsBackupEncrypted } = appState.accountMenu; | ||
|
||
const downloadDataArchive = () => { | ||
application.getArchiveService().downloadBackup(isBackupEncrypted); | ||
}; | ||
|
||
const readFile = async (file: File): Promise<any> => { | ||
return new Promise((resolve) => { | ||
const reader = new FileReader(); | ||
reader.onload = (e) => { | ||
try { | ||
const data = JSON.parse(e.target!.result as string); | ||
resolve(data); | ||
} catch (e) { | ||
application.alertService.alert(STRING_INVALID_IMPORT_FILE); | ||
} | ||
}; | ||
reader.readAsText(file); | ||
}); | ||
}; | ||
|
||
const performImport = async (data: BackupFile) => { | ||
setIsImportDataLoading(true); | ||
|
||
const result = await application.importData(data); | ||
|
||
setIsImportDataLoading(false); | ||
|
||
if (!result) { | ||
return; | ||
} | ||
|
||
let statusText = STRING_IMPORT_SUCCESS; | ||
if ('error' in result) { | ||
statusText = result.error; | ||
} else if (result.errorCount) { | ||
statusText = StringImportError(result.errorCount); | ||
} | ||
void alertDialog({ | ||
text: statusText | ||
}); | ||
}; | ||
|
||
const importFileSelected = async (event: TargetedEvent<HTMLInputElement, Event>) => { | ||
const { files } = (event.target as HTMLInputElement); | ||
|
||
if (!files) { | ||
return; | ||
} | ||
const file = files[0]; | ||
const data = await readFile(file); | ||
if (!data) { | ||
return; | ||
} | ||
|
||
const version = data.version || data.keyParams?.version || data.auth_params?.version; | ||
if (!version) { | ||
await performImport(data); | ||
return; | ||
} | ||
|
||
if ( | ||
application.protocolService.supportedVersions().includes(version) | ||
) { | ||
await performImport(data); | ||
} else { | ||
setIsImportDataLoading(false); | ||
void alertDialog({ text: STRING_UNSUPPORTED_BACKUP_FILE_VERSION }); | ||
} | ||
}; | ||
|
||
// Whenever "Import Backup" is either clicked or key-pressed, proceed the import | ||
const handleImportFile = (event: TargetedEvent<HTMLSpanElement, Event> | KeyboardEvent) => { | ||
if (event instanceof KeyboardEvent) { | ||
const { code } = event; | ||
|
||
// Process only when "Enter" or "Space" keys are pressed | ||
if (code !== 'Enter' && code !== 'Space') { | ||
return; | ||
} | ||
// Don't proceed the event's default action | ||
// (like scrolling in case the "space" key is pressed) | ||
event.preventDefault(); | ||
} | ||
|
||
(fileInputRef.current as HTMLInputElement).click(); | ||
}; | ||
|
||
return ( | ||
<> | ||
<PreferencesGroup> | ||
<PreferencesSegment> | ||
<Title>Data Backups</Title> | ||
|
||
{!isDesktopApplication() && ( | ||
<Text className="mb-3"> | ||
Backups are automatically created on desktop and can be managed | ||
via the "Backups" top-level menu. | ||
</Text> | ||
)} | ||
|
||
<Subtitle>Download a backup of all your data</Subtitle> | ||
|
||
{isEncryptionEnabled && ( | ||
<form className="sk-panel-form sk-panel-row"> | ||
<div className="sk-input-group"> | ||
<label className="sk-horizontal-group tight"> | ||
<input | ||
type="radio" | ||
onChange={() => setIsBackupEncrypted(true)} | ||
checked={isBackupEncrypted} | ||
/> | ||
<Subtitle>Encrypted</Subtitle> | ||
</label> | ||
<label className="sk-horizontal-group tight"> | ||
<input | ||
type="radio" | ||
onChange={() => setIsBackupEncrypted(false)} | ||
checked={!isBackupEncrypted} | ||
/> | ||
<Subtitle>Decrypted</Subtitle> | ||
</label> | ||
</div> | ||
</form> | ||
)} | ||
|
||
<Button type="normal" onClick={downloadDataArchive} label="Download backup" className="mt-2" /> | ||
|
||
</PreferencesSegment> | ||
<PreferencesSegment> | ||
|
||
<Subtitle>Import a previously saved backup file</Subtitle> | ||
|
||
<div class="flex flex-row items-center mt-3" > | ||
<Button type="normal" label="Import Backup" onClick={handleImportFile} /> | ||
<input | ||
type="file" | ||
ref={fileInputRef} | ||
onChange={importFileSelected} | ||
className="hidden" | ||
/> | ||
{isImportDataLoading && <div className="sk-spinner normal info ml-4" />} | ||
</div> | ||
|
||
</PreferencesSegment> | ||
|
||
</PreferencesGroup> | ||
</> | ||
); | ||
}); |
1 change: 1 addition & 0 deletions
1
app/assets/javascripts/preferences/panes/security-segments/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from './Encryption'; | ||
export * from './PasscodeLock'; | ||
export * from './Protections'; | ||
export * from './DataBackups'; |