diff --git a/components/settings/data-management/index.js b/components/settings/data-management/DataManagement.js
similarity index 53%
rename from components/settings/data-management/index.js
rename to components/settings/data-management/DataManagement.js
index d36ad70..1e4d2a3 100644
--- a/components/settings/data-management/index.js
+++ b/components/settings/data-management/DataManagement.js
@@ -6,40 +6,23 @@ import AppText from '../../common/app-text'
import Button from '../../common/button'
import Segment from '../../common/segment'
-import { openImportDialog, getFileContent, importData } from './import-dialog'
import openShareDialogAndExport from './export-dialog'
import DeleteData from './delete-data'
import labels from '../../../i18n/en/settings'
-import { ACTION_DELETE, ACTION_EXPORT, ACTION_IMPORT } from '../../../config'
+import ImportData from './ImportData'
const DataManagement = () => {
const [isLoading, setIsLoading] = useState(false)
- const [currentAction, setCurrentAction] = useState(null)
-
- const startImportFlow = async (shouldDeleteExistingData) => {
- setIsLoading(true)
- const fileContent = await getFileContent()
- if (fileContent) {
- await importData(shouldDeleteExistingData, fileContent)
- }
- setIsLoading(false)
- }
+ const [isDeletingData, setIsDeletingData] = useState(false)
const startExport = () => {
- setCurrentAction(ACTION_EXPORT)
+ setIsDeletingData(false)
openShareDialogAndExport()
}
- const startImport = () => {
- setCurrentAction(ACTION_IMPORT)
- openImportDialog(startImportFlow)
- }
-
if (isLoading) return
- const isDeletingData = currentAction === ACTION_DELETE
-
return (
@@ -48,17 +31,15 @@ const DataManagement = () => {
{labels.export.button}
-
- {labels.import.segmentExplainer}
-
-
+ setIsDeletingData(false)}
+ setIsLoading={setIsLoading}
+ />
{labels.deleteSegment.explainer}
setCurrentAction(ACTION_DELETE)}
+ onStartDeletion={() => setIsDeletingData(true)}
/>
diff --git a/components/settings/data-management/ImportData.js b/components/settings/data-management/ImportData.js
new file mode 100644
index 0000000..f70c9a3
--- /dev/null
+++ b/components/settings/data-management/ImportData.js
@@ -0,0 +1,97 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Alert } from 'react-native'
+import DocumentPicker from 'react-native-document-picker'
+import rnfs from 'react-native-fs'
+import importCsv from '../../../lib/import-export/import-from-csv'
+import alertError from '../common/alert-error'
+import Segment from '../../common/segment'
+import AppText from '../../common/app-text'
+import Button from '../../common/button'
+import { useTranslation } from 'react-i18next'
+
+export default function ImportData({ resetIsDeletingData, setIsLoading }) {
+ const { t } = useTranslation(null, {
+ keyPrefix: 'hamburgerMenu.settings.data.import',
+ })
+
+ async function startImport(shouldDeleteExistingData) {
+ setIsLoading(true)
+ await importData(shouldDeleteExistingData)
+ setIsLoading(false)
+ }
+
+ async function getFileInfo() {
+ try {
+ const fileInfo = await DocumentPicker.pickSingle({
+ type: [DocumentPicker.types.csv, 'text/comma-separated-values'],
+ })
+ return fileInfo
+ } catch (error) {
+ if (DocumentPicker.isCancel(error)) return // User cancelled the picker, exit any dialogs or menus and move on
+ showImportErrorAlert(error)
+ }
+ }
+
+ async function getFileContent() {
+ const fileInfo = await getFileInfo()
+ if (!fileInfo) return null
+
+ try {
+ const fileContent = await rnfs.readFile(fileInfo.uri, 'utf8')
+ return fileContent
+ } catch (err) {
+ return showImportErrorAlert(t('error.couldNotOpenFile'))
+ }
+ }
+
+ async function importData(shouldDeleteExistingData) {
+ const fileContent = await getFileContent()
+ if (!fileContent) return
+
+ try {
+ await importCsv(fileContent, shouldDeleteExistingData)
+ Alert.alert(t('success.title'), t('success.message'))
+ } catch (err) {
+ showImportErrorAlert(err.message)
+ }
+ }
+
+ function openImportDialog() {
+ resetIsDeletingData()
+ Alert.alert(t('dialog.title'), t('dialog.message'), [
+ {
+ text: t('dialog.cancel'),
+ style: 'cancel',
+ onPress: () => {},
+ },
+ {
+ text: t('dialog.replace'),
+ onPress: () => startImport(false),
+ },
+ {
+ text: t('dialog.delete'),
+ onPress: () => startImport(true),
+ },
+ ])
+ }
+
+ function showImportErrorAlert(message) {
+ const errorMessage = t('error.noDataImported', { message })
+ alertError(errorMessage)
+ }
+
+ return (
+
+ {t('segmentExplainer')}
+
+
+ )
+}
+
+ImportData.propTypes = {
+ resetIsDeletingData: PropTypes.func.isRequired,
+ setIsLoading: PropTypes.func.isRequired,
+}
diff --git a/components/settings/data-management/import-dialog.js b/components/settings/data-management/import-dialog.js
deleted file mode 100644
index bd445b1..0000000
--- a/components/settings/data-management/import-dialog.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import { Alert } from 'react-native'
-import DocumentPicker from 'react-native-document-picker'
-import rnfs from 'react-native-fs'
-import importCsv from '../../../lib/import-export/import-from-csv'
-import { shared as sharedLabels } from '../../../i18n/en/labels'
-import labels from '../../../i18n/en/settings'
-import alertError from '../common/alert-error'
-
-export function openImportDialog(onImportData) {
- Alert.alert(labels.import.title, labels.import.message, [
- {
- text: sharedLabels.cancel,
- style: 'cancel',
- onPress: () => {},
- },
- {
- text: labels.import.replaceOption,
- onPress: () => onImportData(false),
- },
- {
- text: labels.import.deleteOption,
- onPress: () => onImportData(true),
- },
- ])
-}
-
-export async function getFileContent() {
- let fileInfo
- try {
- fileInfo = await DocumentPicker.pickSingle({
- type: [DocumentPicker.types.csv, 'text/comma-separated-values'],
- })
- } catch (error) {
- if (DocumentPicker.isCancel(error)) {
- // User cancelled the picker, exit any dialogs or menus and move on
- return
- } else {
- importError(error)
- }
- }
-
- let fileContent
- try {
- fileContent = await rnfs.readFile(fileInfo.uri, 'utf8')
- } catch (err) {
- return importError(labels.import.errors.couldNotOpenFile)
- }
-
- return fileContent
-}
-
-export async function importData(shouldDeleteExistingData, fileContent) {
- try {
- await importCsv(fileContent, shouldDeleteExistingData)
- Alert.alert(sharedLabels.successTitle, labels.import.success.message)
- } catch (err) {
- importError(err.message)
- }
-}
-
-function importError(msg) {
- const postFixed = `${msg}\n\n${labels.import.errors.postFix}`
- alertError(postFixed)
-}
diff --git a/components/settings/index.js b/components/settings/index.js
index 2ee7985..bdbc941 100644
--- a/components/settings/index.js
+++ b/components/settings/index.js
@@ -1,6 +1,6 @@
import Reminders from './reminders/reminders'
import NfpSettings from './nfp-settings'
-import DataManagement from './data-management'
+import DataManagement from './data-management/DataManagement'
import Password from './password'
import About from './About'
import License from './License'
diff --git a/config.js b/config.js
index 7b4fa8e..d7db960 100644
--- a/config.js
+++ b/config.js
@@ -1,10 +1,6 @@
import { PixelRatio, StatusBar } from 'react-native'
import { scale, verticalScale } from 'react-native-size-matters'
-export const ACTION_DELETE = 'delete'
-export const ACTION_EXPORT = 'export'
-export const ACTION_IMPORT = 'import'
-
export const SYMPTOMS = [
'bleeding',
'temperature',
@@ -40,7 +36,7 @@ export const HIT_SLOP = {
top: verticalScale(20),
bottom: verticalScale(20),
left: scale(20),
- right: scale(20)
+ right: scale(20),
}
export const STATUSBAR_HEIGHT = StatusBar.currentHeight
diff --git a/i18n/en.json b/i18n/en.json
index 2f61800..305c95b 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -88,6 +88,29 @@
}
},
"settings": {
+ "data": {
+ "import": {
+ "button": "Import data",
+ "dialog": {
+ "cancel": "Cancel",
+ "delete": "Import and delete existing",
+ "message": "There are two options for the import:\n\n1. Keep existing cycle days and replace only the ones in the import file.\n\n2. Delete all existing cycle days and import cycle days from file",
+ "replace": "Import and replace",
+ "title": "Keep existing data?"
+ },
+ "error": {
+ "couldNotOpenFile": "Could not open file",
+ "futureEdit": "Future dates may only contain a note, no other symptoms",
+ "incorrectColumns": "Expected CSV column titles to be {{incorrectColumns}}",
+ "noDataImported": "{{message}}\n\nNo data was imported or changed"
+ },
+ "segmentExplainer": "Import data in CSV format",
+ "success": {
+ "message": "Data successfully imported",
+ "title": "Success"
+ }
+ }
+ },
"menuItem": {
"dataManagement": {
"name": "Data",
diff --git a/i18n/en/settings.js b/i18n/en/settings.js
index 6a07de6..e330edc 100644
--- a/i18n/en/settings.js
+++ b/i18n/en/settings.js
@@ -13,24 +13,6 @@ export default {
segmentExplainer:
'Export data in CSV format for backup or so you can use it elsewhere',
},
- import: {
- button: 'Import data',
- title: 'Keep existing data?',
- message: `There are two options for the import:
-1. Keep existing cycle days and replace only the ones in the import file.
-2. Delete all existing cycle days and import cycle days from file.`,
- replaceOption: 'Import and replace',
- deleteOption: 'Import and delete existing',
- errors: {
- couldNotOpenFile: 'Could not open file',
- postFix: 'No data was imported or changed',
- futureEdit: 'Future dates may only contain a note, no other symptoms',
- },
- success: {
- message: 'Data successfully imported',
- },
- segmentExplainer: 'Import data in CSV format',
- },
deleteSegment: {
title: 'Delete app data',
explainer: 'Delete app data from this phone',
diff --git a/lib/import-export/import-from-csv.js b/lib/import-export/import-from-csv.js
index 80e9bbc..2a776a2 100644
--- a/lib/import-export/import-from-csv.js
+++ b/lib/import-export/import-from-csv.js
@@ -8,7 +8,7 @@ import {
import getColumnNamesForCsv from './get-csv-column-names'
import replaceWithNullIfAllPropertiesAreNull from './replace-with-null'
import { LocalDate } from '@js-joda/core'
-import labels from '../../i18n/en/settings'
+import i18next from 'i18next'
export default async function importCsv(csv, deleteFirst) {
const parseFuncs = {
@@ -46,7 +46,10 @@ export default async function importCsv(csv, deleteFirst) {
const cycleDays = await csvParser(config)
.fromString(csv)
- .on('header', validateHeaders)
+ .on('header', (headers) => validateHeaders(headers))
+ .on('error', (error) => {
+ throw error
+ })
//remove symptoms where all fields are null
putNullForEmptySymptoms(cycleDays)
@@ -67,8 +70,11 @@ function validateHeaders(headers) {
return expectedHeaders.indexOf(header) > -1
})
) {
- const msg = `Expected CSV column titles to be ${expectedHeaders.join()}`
- throw new Error(msg)
+ throw new Error(
+ i18next.t('hamburgerMenu.settings.data.import.error.incorrectColumns', {
+ incorrectColumns: expectedHeaders.join(),
+ })
+ )
}
}
@@ -92,7 +98,9 @@ function throwIfFutureData(cycleDays) {
day.date > today &&
Object.keys(day).some((symptom) => symptom != 'date' && symptom != 'note')
) {
- throw new Error(labels.import.errors.futureEdit)
+ throw new Error(
+ i18next.t('hamburgerMenu.settings.data.import.error.futureEdit')
+ )
}
}
}