From 446638d6dea1cfcb1a3d97c5f44d3c185ee809f0 Mon Sep 17 00:00:00 2001 From: Lisa Date: Sun, 6 Nov 2022 14:47:05 +0000 Subject: [PATCH] 618 Refactor import section to use translation lib --- .../{index.js => DataManagement.js} | 35 ++----- .../settings/data-management/ImportData.js | 97 +++++++++++++++++++ .../settings/data-management/import-dialog.js | 64 ------------ components/settings/index.js | 2 +- config.js | 6 +- i18n/en.json | 23 +++++ i18n/en/settings.js | 18 ---- lib/import-export/import-from-csv.js | 18 +++- 8 files changed, 143 insertions(+), 120 deletions(-) rename components/settings/data-management/{index.js => DataManagement.js} (53%) create mode 100644 components/settings/data-management/ImportData.js delete mode 100644 components/settings/data-management/import-dialog.js 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 b67c06d..2ef5581 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -85,6 +85,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') + ) } } }