Add ability to import Actual files; enable export on desktop
This commit is contained in:
parent
58494fe174
commit
d7e71158b9
9 changed files with 264 additions and 137 deletions
|
@ -334,7 +334,7 @@ function FileSettings({
|
||||||
|
|
||||||
async function onExport() {
|
async function onExport() {
|
||||||
let data = await send('export-budget');
|
let data = await send('export-budget');
|
||||||
window.Actual.saveFile(data, 'budget.zip', 'Export budget');
|
window.Actual.saveFile(data, `${prefs.id}.zip`, 'Export budget');
|
||||||
}
|
}
|
||||||
|
|
||||||
let dateFormat = prefs.dateFormat || 'MM/dd/yyyy';
|
let dateFormat = prefs.dateFormat || 'MM/dd/yyyy';
|
||||||
|
@ -431,12 +431,10 @@ function FileSettings({
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{Platform.isBrowser && (
|
|
||||||
<View style={{ marginTop: 30, alignItems: 'flex-start' }}>
|
<View style={{ marginTop: 30, alignItems: 'flex-start' }}>
|
||||||
<Title name="Export" />
|
<Title name="Export" />
|
||||||
<Button onClick={onExport}>Export data</Button>
|
<Button onClick={onExport}>Export data</Button>
|
||||||
</View>
|
</View>
|
||||||
)}
|
|
||||||
|
|
||||||
<Advanced
|
<Advanced
|
||||||
prefs={prefs}
|
prefs={prefs}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import LoadBackup from 'loot-design/src/components/modals/LoadBackup';
|
||||||
import Import from 'loot-design/src/components/manager/Import';
|
import Import from 'loot-design/src/components/manager/Import';
|
||||||
import ImportYNAB4 from 'loot-design/src/components/manager/ImportYNAB4';
|
import ImportYNAB4 from 'loot-design/src/components/manager/ImportYNAB4';
|
||||||
import ImportYNAB5 from 'loot-design/src/components/manager/ImportYNAB5';
|
import ImportYNAB5 from 'loot-design/src/components/manager/ImportYNAB5';
|
||||||
|
import ImportActual from 'loot-design/src/components/manager/ImportActual';
|
||||||
import DeleteFile from 'loot-design/src/components/manager/DeleteFile';
|
import DeleteFile from 'loot-design/src/components/manager/DeleteFile';
|
||||||
import CreateEncryptionKey from '../modals/CreateEncryptionKey';
|
import CreateEncryptionKey from '../modals/CreateEncryptionKey';
|
||||||
import FixEncryptionKey from '../modals/FixEncryptionKey';
|
import FixEncryptionKey from '../modals/FixEncryptionKey';
|
||||||
|
@ -72,6 +73,10 @@ function Modals({
|
||||||
return (
|
return (
|
||||||
<ImportYNAB5 key={name} modalProps={modalProps} actions={actions} />
|
<ImportYNAB5 key={name} modalProps={modalProps} actions={actions} />
|
||||||
);
|
);
|
||||||
|
case 'import-actual':
|
||||||
|
return (
|
||||||
|
<ImportActual key={name} modalProps={modalProps} actions={actions} />
|
||||||
|
);
|
||||||
case 'load-backup': {
|
case 'load-backup': {
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
|
|
|
@ -8,7 +8,7 @@ let socketClient = null;
|
||||||
function connectSocket(name, onOpen) {
|
function connectSocket(name, onOpen) {
|
||||||
global.Actual.ipcConnect(name, function(client) {
|
global.Actual.ipcConnect(name, function(client) {
|
||||||
client.on('message', data => {
|
client.on('message', data => {
|
||||||
const msg = JSON.parse(data);
|
const msg = data;
|
||||||
|
|
||||||
if (msg.type === 'error') {
|
if (msg.type === 'error') {
|
||||||
// An error happened while handling a message so cleanup the
|
// An error happened while handling a message so cleanup the
|
||||||
|
@ -19,7 +19,15 @@ function connectSocket(name, onOpen) {
|
||||||
const { id } = msg;
|
const { id } = msg;
|
||||||
replyHandlers.delete(id);
|
replyHandlers.delete(id);
|
||||||
} else if (msg.type === 'reply') {
|
} else if (msg.type === 'reply') {
|
||||||
const { id, result, mutated, undoTag } = msg;
|
let { id, result, mutated, undoTag } = msg;
|
||||||
|
|
||||||
|
// Check if the result is a serialized buffer, and if so
|
||||||
|
// convert it to a Uint8Array. This is only needed when working
|
||||||
|
// with node; the web version connection layer automatically
|
||||||
|
// supports buffers
|
||||||
|
if (result && result.type === 'Buffer' && Array.isArray(result.data)) {
|
||||||
|
result = new Uint8Array(result.data);
|
||||||
|
}
|
||||||
|
|
||||||
const handler = replyHandlers.get(id);
|
const handler = replyHandlers.get(id);
|
||||||
if (handler) {
|
if (handler) {
|
||||||
|
@ -53,9 +61,7 @@ function connectSocket(name, onOpen) {
|
||||||
|
|
||||||
// Send any messages that were queued while closed
|
// Send any messages that were queued while closed
|
||||||
if (messageQueue.length > 0) {
|
if (messageQueue.length > 0) {
|
||||||
messageQueue.forEach(msg =>
|
messageQueue.forEach(msg => client.emit('message', msg));
|
||||||
client.emit('message', JSON.stringify(msg))
|
|
||||||
);
|
|
||||||
messageQueue = [];
|
messageQueue = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,16 +84,13 @@ module.exports.send = function send(name, args, { catchErrors = false } = {}) {
|
||||||
replyHandlers.set(id, { resolve, reject });
|
replyHandlers.set(id, { resolve, reject });
|
||||||
|
|
||||||
if (socketClient) {
|
if (socketClient) {
|
||||||
socketClient.emit(
|
socketClient.emit('message', {
|
||||||
'message',
|
|
||||||
JSON.stringify({
|
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
args,
|
args,
|
||||||
undoTag: undo.snapshot(),
|
undoTag: undo.snapshot(),
|
||||||
catchErrors: !!catchErrors
|
catchErrors: !!catchErrors
|
||||||
})
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
messageQueue.push({
|
messageQueue.push({
|
||||||
id,
|
id,
|
||||||
|
|
|
@ -16,7 +16,7 @@ function init(socketName, handlers) {
|
||||||
|
|
||||||
ipc.serve(() => {
|
ipc.serve(() => {
|
||||||
ipc.server.on('message', (data, socket) => {
|
ipc.server.on('message', (data, socket) => {
|
||||||
let msg = JSON.parse(data);
|
let msg = data;
|
||||||
let { id, name, args, undoTag, catchErrors } = msg;
|
let { id, name, args, undoTag, catchErrors } = msg;
|
||||||
|
|
||||||
if (handlers[name]) {
|
if (handlers[name]) {
|
||||||
|
@ -26,10 +26,7 @@ function init(socketName, handlers) {
|
||||||
result = { data: result, error: null };
|
result = { data: result, error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
ipc.server.emit(
|
ipc.server.emit(socket, 'message', {
|
||||||
socket,
|
|
||||||
'message',
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'reply',
|
type: 'reply',
|
||||||
id,
|
id,
|
||||||
result,
|
result,
|
||||||
|
@ -38,8 +35,7 @@ function init(socketName, handlers) {
|
||||||
name !== 'undo' &&
|
name !== 'undo' &&
|
||||||
name !== 'redo',
|
name !== 'redo',
|
||||||
undoTag
|
undoTag
|
||||||
})
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
nativeError => {
|
nativeError => {
|
||||||
let error = coerceError(nativeError);
|
let error = coerceError(nativeError);
|
||||||
|
@ -47,27 +43,15 @@ function init(socketName, handlers) {
|
||||||
if (name.startsWith('api/')) {
|
if (name.startsWith('api/')) {
|
||||||
// The API is newer and does automatically forward
|
// The API is newer and does automatically forward
|
||||||
// errors
|
// errors
|
||||||
ipc.server.emit(
|
ipc.server.emit(socket, 'message', { type: 'reply', id, error });
|
||||||
socket,
|
|
||||||
'message',
|
|
||||||
JSON.stringify({ type: 'reply', id, error })
|
|
||||||
);
|
|
||||||
} else if (catchErrors) {
|
} else if (catchErrors) {
|
||||||
ipc.server.emit(
|
ipc.server.emit(socket, 'message', {
|
||||||
socket,
|
|
||||||
'message',
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'reply',
|
type: 'reply',
|
||||||
id,
|
id,
|
||||||
result: { error, data: null }
|
result: { error, data: null }
|
||||||
})
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
ipc.server.emit(
|
ipc.server.emit(socket, 'message', { type: 'error', id });
|
||||||
socket,
|
|
||||||
'message',
|
|
||||||
JSON.stringify({ type: 'error', id })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.type === 'InternalError' && name !== 'api/load-budget') {
|
if (error.type === 'InternalError' && name !== 'api/load-budget') {
|
||||||
|
@ -83,16 +67,12 @@ function init(socketName, handlers) {
|
||||||
} else {
|
} else {
|
||||||
console.warn('Unknown method: ' + name);
|
console.warn('Unknown method: ' + name);
|
||||||
captureException(new Error('Unknown server method: ' + name));
|
captureException(new Error('Unknown server method: ' + name));
|
||||||
ipc.server.emit(
|
ipc.server.emit(socket, 'message', {
|
||||||
socket,
|
|
||||||
'message',
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'reply',
|
type: 'reply',
|
||||||
id,
|
id,
|
||||||
result: null,
|
result: null,
|
||||||
error: { type: 'APIError', message: 'Unknown method: ' + name }
|
error: { type: 'APIError', message: 'Unknown method: ' + name }
|
||||||
})
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -106,10 +86,7 @@ function getNumClients() {
|
||||||
|
|
||||||
function send(name, args) {
|
function send(name, args) {
|
||||||
if (ipc.server) {
|
if (ipc.server) {
|
||||||
ipc.server.broadcast(
|
ipc.server.broadcast('message', { type: 'push', name, args });
|
||||||
'message',
|
|
||||||
JSON.stringify({ type: 'push', name, args })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,15 @@ function pathToId(filepath) {
|
||||||
|
|
||||||
function _exists(filepath) {
|
function _exists(filepath) {
|
||||||
try {
|
try {
|
||||||
FS.stat(filepath);
|
FS.readlink(filepath);
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
FS.stat(filepath);
|
||||||
|
return true;
|
||||||
|
} catch (e) {}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _mkdirRecursively(dir) {
|
function _mkdirRecursively(dir) {
|
||||||
|
|
|
@ -154,7 +154,60 @@ export async function exportBuffer() {
|
||||||
zipped.addFile('metadata.json', metaContent);
|
zipped.addFile('metadata.json', metaContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
return zipped.toBuffer();
|
return Buffer.from(zipped.toBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importBuffer(fileData, buffer) {
|
||||||
|
let zipped = new AdmZip(buffer);
|
||||||
|
let entries = zipped.getEntries();
|
||||||
|
let dbEntry = entries.find(e => e.entryName.includes('db.sqlite'));
|
||||||
|
let metaEntry = entries.find(e => e.entryName.includes('metadata.json'));
|
||||||
|
|
||||||
|
if (!dbEntry || !metaEntry) {
|
||||||
|
throw FileDownloadError('invalid-zip-file');
|
||||||
|
}
|
||||||
|
|
||||||
|
let dbContent = zipped.readFile(dbEntry);
|
||||||
|
let metaContent = zipped.readFile(metaEntry);
|
||||||
|
|
||||||
|
let meta;
|
||||||
|
try {
|
||||||
|
meta = JSON.parse(metaContent.toString('utf8'));
|
||||||
|
} catch (err) {
|
||||||
|
throw FileDownloadError('invalid-meta-file');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the metadata. The stored file on the server might be
|
||||||
|
// out-of-date with a few keys
|
||||||
|
meta = {
|
||||||
|
...meta,
|
||||||
|
cloudFileId: fileData.fileId,
|
||||||
|
groupId: fileData.groupId,
|
||||||
|
lastUploaded: monthUtils.currentDay(),
|
||||||
|
encryptKeyId: fileData.encryptMeta ? fileData.encryptMeta.keyId : null
|
||||||
|
};
|
||||||
|
|
||||||
|
let budgetDir = fs.getBudgetDir(meta.id);
|
||||||
|
|
||||||
|
if (await fs.exists(budgetDir)) {
|
||||||
|
// Don't remove the directory so that backups are retained
|
||||||
|
let dbFile = fs.join(budgetDir, 'db.sqlite');
|
||||||
|
let metaFile = fs.join(budgetDir, 'metadata.json');
|
||||||
|
|
||||||
|
if (await fs.exists(dbFile)) {
|
||||||
|
await fs.removeFile(dbFile);
|
||||||
|
}
|
||||||
|
if (await fs.exists(metaFile)) {
|
||||||
|
await fs.removeFile(metaFile);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await fs.mkdir(budgetDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(fs.join(budgetDir, 'db.sqlite'), dbContent);
|
||||||
|
await fs.writeFile(fs.join(budgetDir, 'metadata.json'), JSON.stringify(meta));
|
||||||
|
|
||||||
|
return { id: meta.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function upload() {
|
export async function upload() {
|
||||||
|
@ -355,58 +408,5 @@ export async function download(fileId, replace) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let zipped = new AdmZip(buffer);
|
return importBuffer(fileData, buffer, replace);
|
||||||
let entries = zipped.getEntries();
|
|
||||||
let dbEntry = entries.find(e => e.entryName.includes('db.sqlite'));
|
|
||||||
let metaEntry = entries.find(e => e.entryName.includes('metadata.json'));
|
|
||||||
|
|
||||||
if (!dbEntry || !metaEntry) {
|
|
||||||
throw FileDownloadError('invalid-zip-file');
|
|
||||||
}
|
|
||||||
|
|
||||||
let dbContent = zipped.readFile(dbEntry);
|
|
||||||
let metaContent = zipped.readFile(metaEntry);
|
|
||||||
|
|
||||||
let meta;
|
|
||||||
try {
|
|
||||||
meta = JSON.parse(metaContent.toString('utf8'));
|
|
||||||
} catch (err) {
|
|
||||||
throw FileDownloadError('invalid-meta-file');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the metadata. The stored file on the server might be
|
|
||||||
// out-of-date with a few keys
|
|
||||||
meta = {
|
|
||||||
...meta,
|
|
||||||
cloudFileId: fileData.fileId,
|
|
||||||
groupId: fileData.groupId,
|
|
||||||
lastUploaded: monthUtils.currentDay(),
|
|
||||||
encryptKeyId: fileData.encryptMeta ? fileData.encryptMeta.keyId : null
|
|
||||||
};
|
|
||||||
|
|
||||||
let budgetDir = fs.getBudgetDir(meta.id);
|
|
||||||
|
|
||||||
if (await fs.exists(budgetDir)) {
|
|
||||||
if (replace) {
|
|
||||||
// Don't remove the directory so that backups are retained
|
|
||||||
let dbFile = fs.join(budgetDir, 'db.sqlite');
|
|
||||||
let metaFile = fs.join(budgetDir, 'metadata.json');
|
|
||||||
|
|
||||||
if (await fs.exists(dbFile)) {
|
|
||||||
await fs.removeFile(dbFile);
|
|
||||||
}
|
|
||||||
if (await fs.exists(metaFile)) {
|
|
||||||
await fs.removeFile(metaFile);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw FileDownloadError('file-exists', { id: meta.id });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await fs.mkdir(budgetDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(fs.join(budgetDir, 'db.sqlite'), dbContent);
|
|
||||||
await fs.writeFile(fs.join(budgetDir, 'metadata.json'), JSON.stringify(meta));
|
|
||||||
|
|
||||||
return { id: meta.id };
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1834,6 +1834,41 @@ handlers['import-budget'] = async function({ filepath, type }) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { error: 'not-ynab5' };
|
return { error: 'not-ynab5' };
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
case 'actual':
|
||||||
|
// We should pull out import/export into its own app so this
|
||||||
|
// can be abstracted out better. Importing Actual files is a
|
||||||
|
// special case because we can directly write down the files,
|
||||||
|
// but because it doesn't go through the API layer we need to
|
||||||
|
// duplicate some of the workflow
|
||||||
|
await handlers['close-budget']();
|
||||||
|
|
||||||
|
let { id } = await cloudStorage.importBuffer(
|
||||||
|
{ cloudFileId: null, groupId: null },
|
||||||
|
buffer
|
||||||
|
);
|
||||||
|
|
||||||
|
// We never want to load cached data from imported files, so
|
||||||
|
// delete the cache
|
||||||
|
let sqliteDb = await sqlite.openDatabase(
|
||||||
|
fs.join(fs.getBudgetDir(id), 'db.sqlite')
|
||||||
|
);
|
||||||
|
sqlite.execQuery(
|
||||||
|
sqliteDb,
|
||||||
|
`
|
||||||
|
DELETE FROM kvcache;
|
||||||
|
DELETE FROM kvcache_key;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
sqlite.closeDatabase(sqliteDb);
|
||||||
|
|
||||||
|
// Load the budget, force everything to be computed, and try
|
||||||
|
// to upload it as a cloud file
|
||||||
|
await handlers['load-budget']({ id });
|
||||||
|
await handlers['get-budget-bounds']();
|
||||||
|
await sheet.waitOnSpreadsheet();
|
||||||
|
await cloudStorage.upload().catch(err => {});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,9 @@ function Import({ modalProps, actions, availableImports }) {
|
||||||
case 'ynab5':
|
case 'ynab5':
|
||||||
actions.pushModal('import-ynab5');
|
actions.pushModal('import-ynab5');
|
||||||
break;
|
break;
|
||||||
|
case 'actual':
|
||||||
|
actions.pushModal('import-actual');
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,14 +86,12 @@ function Import({ modalProps, actions, availableImports }) {
|
||||||
<div>The newer web app</div>
|
<div>The newer web app</div>
|
||||||
</View>
|
</View>
|
||||||
</Button>
|
</Button>
|
||||||
{false && (
|
<Button style={itemStyle} onClick={() => onSelectType('actual')}>
|
||||||
<Button style={itemStyle}>
|
|
||||||
<span style={{ fontWeight: 700 }}>Actual</span>
|
<span style={{ fontWeight: 700 }}>Actual</span>
|
||||||
<View style={{ color: colors.n5 }}>
|
<View style={{ color: colors.n5 }}>
|
||||||
<div>Import a backup or external file</div>
|
<div>Import a file exported from Actual</div>
|
||||||
</View>
|
</View>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
104
packages/loot-design/src/components/manager/ImportActual.js
Normal file
104
packages/loot-design/src/components/manager/ImportActual.js
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { importBudget } from 'loot-core/src/client/actions/budgets';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Block,
|
||||||
|
Modal,
|
||||||
|
ButtonWithLoading,
|
||||||
|
Button,
|
||||||
|
Link,
|
||||||
|
P,
|
||||||
|
ExternalLink
|
||||||
|
} from '../common';
|
||||||
|
import { styles, colors } from '../../style';
|
||||||
|
|
||||||
|
function getErrorMessage(error) {
|
||||||
|
switch (error) {
|
||||||
|
case 'parse-error':
|
||||||
|
return 'Unable to parse file. Please select a JSON file exported from nYNAB.';
|
||||||
|
case 'not-ynab5':
|
||||||
|
return 'This file is not valid. Please select a JSON file exported from nYNAB.';
|
||||||
|
default:
|
||||||
|
return 'An unknown error occurred while importing. Sorry! We have been notified of this issue.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Import({ modalProps, availableImports }) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
const [importing, setImporting] = useState(false);
|
||||||
|
|
||||||
|
async function onImport() {
|
||||||
|
const res = await window.Actual.openFileDialog({
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: [{ name: 'actual', extensions: ['zip'] }]
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
setImporting(true);
|
||||||
|
setError(false);
|
||||||
|
try {
|
||||||
|
await dispatch(importBudget(res[0], 'actual'));
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setImporting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
{...modalProps}
|
||||||
|
showHeader={false}
|
||||||
|
showOverlay={false}
|
||||||
|
noAnimation={true}
|
||||||
|
style={{ width: 400 }}
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<View style={[styles.smallText, { lineHeight: 1.5, marginTop: 20 }]}>
|
||||||
|
{error && (
|
||||||
|
<Block style={{ color: colors.r4, marginBottom: 15 }}>
|
||||||
|
{getErrorMessage(error)}
|
||||||
|
</Block>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={{ '& > div': { lineHeight: '1.7em' } }}>
|
||||||
|
<P>
|
||||||
|
You can import data from another Actual account or instance. First
|
||||||
|
export your data from a different account, and it will give you a
|
||||||
|
compressed file. This file is simple zip file that contains the
|
||||||
|
"db.sqlite" and "metadata.json" files.
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<P>Select one of these compressed files and import it here.</P>
|
||||||
|
|
||||||
|
<View style={{ alignSelf: 'center' }}>
|
||||||
|
<ButtonWithLoading loading={importing} primary onClick={onImport}>
|
||||||
|
Select file...
|
||||||
|
</ButtonWithLoading>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginTop: 20,
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ flex: 1 }} />
|
||||||
|
<Button
|
||||||
|
style={{ marginRight: 10 }}
|
||||||
|
onClick={() => modalProps.onBack()}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Import;
|
Loading…
Reference in a new issue