Merge branch '618/Refactor-import-section-to-use-translation-lib' into 'main'
618 Refactor import section to use translation lib See merge request bloodyhealth/drip!555
This commit is contained in:
+8
-27
@@ -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 <AppLoadingView />
|
||||
|
||||
const isDeletingData = currentAction === ACTION_DELETE
|
||||
|
||||
return (
|
||||
<AppPage>
|
||||
<Segment title={labels.export.button}>
|
||||
@@ -48,17 +31,15 @@ const DataManagement = () => {
|
||||
{labels.export.button}
|
||||
</Button>
|
||||
</Segment>
|
||||
<Segment title={labels.import.button}>
|
||||
<AppText>{labels.import.segmentExplainer}</AppText>
|
||||
<Button isCTA onPress={startImport}>
|
||||
{labels.import.button}
|
||||
</Button>
|
||||
</Segment>
|
||||
<ImportData
|
||||
resetIsDeletingData={() => setIsDeletingData(false)}
|
||||
setIsLoading={setIsLoading}
|
||||
/>
|
||||
<Segment title={labels.deleteSegment.title} last>
|
||||
<AppText>{labels.deleteSegment.explainer}</AppText>
|
||||
<DeleteData
|
||||
isDeletingData={isDeletingData}
|
||||
onStartDeletion={() => setCurrentAction(ACTION_DELETE)}
|
||||
onStartDeletion={() => setIsDeletingData(true)}
|
||||
/>
|
||||
</Segment>
|
||||
</AppPage>
|
||||
@@ -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 (
|
||||
<Segment title={t('button')}>
|
||||
<AppText>{t('segmentExplainer')}</AppText>
|
||||
<Button isCTA onPress={openImportDialog}>
|
||||
{t('button')}
|
||||
</Button>
|
||||
</Segment>
|
||||
)
|
||||
}
|
||||
|
||||
ImportData.propTypes = {
|
||||
resetIsDeletingData: PropTypes.func.isRequired,
|
||||
setIsLoading: PropTypes.func.isRequired,
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user