Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56c783020d | |||
| 43106f0081 | |||
| df6bc575d0 | |||
| d77cb17f1d | |||
| bcc8ebd7e2 | |||
| a54dad9b1d | |||
| bb83785e56 | |||
| 0a80b30388 |
@@ -14,4 +14,3 @@ updates:
|
|||||||
- dependency-name: 'react'
|
- dependency-name: 'react'
|
||||||
- dependency-name: 'react-native'
|
- dependency-name: 'react-native'
|
||||||
- dependency-name: 'react-native-push-notifications'
|
- dependency-name: 'react-native-push-notifications'
|
||||||
- dependency-name: '@babel/core'
|
|
||||||
|
|||||||
@@ -201,3 +201,7 @@ More information about how the app calculates fertility status and bleeding pred
|
|||||||
react-native link
|
react-native link
|
||||||
|
|
||||||
5. You should be able to use the icon now within drip, e.g. in Cycle Day Overview and on the chart.
|
5. You should be able to use the icon now within drip, e.g. in Cycle Day Overview and on the chart.
|
||||||
|
|
||||||
|
## Translation
|
||||||
|
|
||||||
|
We are using [Weblate](https://weblate.org/) as translation software.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import HorizontalGrid from './horizontal-grid'
|
|||||||
import MainGrid from './main-grid'
|
import MainGrid from './main-grid'
|
||||||
import NoData from './no-data'
|
import NoData from './no-data'
|
||||||
import NoTemperature from './no-temperature'
|
import NoTemperature from './no-temperature'
|
||||||
import Tutorial from './Tutorial'
|
import Tutorial from './tutorial'
|
||||||
import YAxis from './y-axis'
|
import YAxis from './y-axis'
|
||||||
|
|
||||||
import { getCycleDaysSortedByDate } from '../../db'
|
import { getCycleDaysSortedByDate } from '../../db'
|
||||||
|
|||||||
@@ -6,17 +6,16 @@ import AppText from '../common/app-text'
|
|||||||
import CloseIcon from '../common/close-icon'
|
import CloseIcon from '../common/close-icon'
|
||||||
|
|
||||||
import { Containers, Spacing } from '../../styles'
|
import { Containers, Spacing } from '../../styles'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { chart } from '../../i18n/en/labels'
|
||||||
|
|
||||||
const image = require('../../assets/swipe.png')
|
const image = require('../../assets/swipe.png')
|
||||||
|
|
||||||
const Tutorial = ({ onClose }) => {
|
const Tutorial = ({ onClose }) => {
|
||||||
const { t } = useTranslation()
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Image resizeMode="contain" source={image} style={styles.image} />
|
<Image resizeMode="contain" source={image} style={styles.image} />
|
||||||
<View style={styles.textContainer}>
|
<View style={styles.textContainer}>
|
||||||
<AppText>{t('chart.tutorial')}</AppText>
|
<AppText>{chart.tutorial}</AppText>
|
||||||
</View>
|
</View>
|
||||||
<CloseIcon onClose={onClose} />
|
<CloseIcon onClose={onClose} />
|
||||||
</View>
|
</View>
|
||||||
@@ -6,23 +6,20 @@ import MenuItem from './menu-item'
|
|||||||
|
|
||||||
import { Containers } from '../../styles'
|
import { Containers } from '../../styles'
|
||||||
import { pages } from '../pages'
|
import { pages } from '../pages'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
const Menu = ({ currentPage, navigate }) => {
|
const Menu = ({ currentPage, navigate }) => {
|
||||||
const menuItems = pages.filter((page) => page.isInMenu)
|
const menuItems = pages.filter((page) => page.isInMenu)
|
||||||
|
|
||||||
const { t } = useTranslation(null, { keyPrefix: 'bottomMenu' })
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{menuItems.map(({ icon, labelKey, component }) => {
|
{menuItems.map(({ icon, label, component }) => {
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
isActive={component === currentPage}
|
isActive={component === currentPage}
|
||||||
onPress={() => navigate(component)}
|
onPress={() => navigate(component)}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
key={labelKey}
|
key={label}
|
||||||
label={t(labelKey)}
|
label={label}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
+15
-3
@@ -1,63 +1,75 @@
|
|||||||
import settingsViews from './settings'
|
import settingsViews from './settings'
|
||||||
|
|
||||||
|
import settingsLabels from '../i18n/en/settings'
|
||||||
|
const labels = settingsLabels.menuItems
|
||||||
|
|
||||||
export const pages = [
|
export const pages = [
|
||||||
{
|
{
|
||||||
component: 'Home',
|
component: 'Home',
|
||||||
icon: 'home',
|
icon: 'home',
|
||||||
|
label: 'Home',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Calendar',
|
component: 'Calendar',
|
||||||
icon: 'calendar',
|
icon: 'calendar',
|
||||||
isInMenu: true,
|
isInMenu: true,
|
||||||
labelKey: 'calendar',
|
label: 'Calendar',
|
||||||
parent: 'Home',
|
parent: 'Home',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Chart',
|
component: 'Chart',
|
||||||
icon: 'chart',
|
icon: 'chart',
|
||||||
isInMenu: true,
|
isInMenu: true,
|
||||||
labelKey: 'chart',
|
label: 'Chart',
|
||||||
parent: 'Home',
|
parent: 'Home',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Stats',
|
component: 'Stats',
|
||||||
icon: 'statistics',
|
icon: 'statistics',
|
||||||
isInMenu: true,
|
isInMenu: true,
|
||||||
labelKey: 'stats',
|
label: 'Stats',
|
||||||
parent: 'Home',
|
parent: 'Home',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
children: Object.keys(settingsViews),
|
children: Object.keys(settingsViews),
|
||||||
component: 'SettingsMenu',
|
component: 'SettingsMenu',
|
||||||
icon: 'settings',
|
icon: 'settings',
|
||||||
|
label: 'Settings',
|
||||||
parent: 'Home',
|
parent: 'Home',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Reminders',
|
component: 'Reminders',
|
||||||
|
label: labels.reminders.name,
|
||||||
parent: 'SettingsMenu',
|
parent: 'SettingsMenu',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'NfpSettings',
|
component: 'NfpSettings',
|
||||||
|
label: labels.nfpSettings.name,
|
||||||
parent: 'SettingsMenu',
|
parent: 'SettingsMenu',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'DataManagement',
|
component: 'DataManagement',
|
||||||
|
label: labels.dataManagement.name,
|
||||||
parent: 'SettingsMenu',
|
parent: 'SettingsMenu',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Password',
|
component: 'Password',
|
||||||
|
label: labels.password.name,
|
||||||
parent: 'SettingsMenu',
|
parent: 'SettingsMenu',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'About',
|
component: 'About',
|
||||||
|
label: 'About',
|
||||||
parent: 'SettingsMenu',
|
parent: 'SettingsMenu',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'License',
|
component: 'License',
|
||||||
|
label: 'License',
|
||||||
parent: 'SettingsMenu',
|
parent: 'SettingsMenu',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'PrivacyPolicy',
|
component: 'PrivacyPolicy',
|
||||||
|
label: 'PrivacyPolicy',
|
||||||
parent: 'SettingsMenu',
|
parent: 'SettingsMenu',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,46 +2,30 @@ import React, { useState } from 'react'
|
|||||||
|
|
||||||
import AppLoadingView from '../../common/app-loading'
|
import AppLoadingView from '../../common/app-loading'
|
||||||
import AppPage from '../../common/app-page'
|
import AppPage from '../../common/app-page'
|
||||||
import AppText from '../../common/app-text'
|
import DeleteData from './DeleteData'
|
||||||
import Button from '../../common/button'
|
|
||||||
import Segment from '../../common/segment'
|
|
||||||
|
|
||||||
import openShareDialogAndExport from './export-dialog'
|
|
||||||
import DeleteData from './delete-data'
|
|
||||||
|
|
||||||
import labels from '../../../i18n/en/settings'
|
|
||||||
import ImportData from './ImportData'
|
import ImportData from './ImportData'
|
||||||
|
import ExportData from './ExportData'
|
||||||
|
|
||||||
const DataManagement = () => {
|
const DataManagement = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [isDeletingData, setIsDeletingData] = useState(false)
|
const [isDeletingData, setIsDeletingData] = useState(false)
|
||||||
|
|
||||||
const startExport = () => {
|
|
||||||
setIsDeletingData(false)
|
|
||||||
openShareDialogAndExport()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) return <AppLoadingView />
|
if (isLoading) return <AppLoadingView />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppPage>
|
<AppPage>
|
||||||
<Segment title={labels.export.button}>
|
<ExportData
|
||||||
<AppText>{labels.export.segmentExplainer}</AppText>
|
resetIsDeletingData={() => setIsDeletingData(false)}
|
||||||
<Button isCTA onPress={startExport}>
|
setIsLoading={setIsLoading}
|
||||||
{labels.export.button}
|
/>
|
||||||
</Button>
|
|
||||||
</Segment>
|
|
||||||
<ImportData
|
<ImportData
|
||||||
resetIsDeletingData={() => setIsDeletingData(false)}
|
resetIsDeletingData={() => setIsDeletingData(false)}
|
||||||
setIsLoading={setIsLoading}
|
setIsLoading={setIsLoading}
|
||||||
/>
|
/>
|
||||||
<Segment title={labels.deleteSegment.title} last>
|
<DeleteData
|
||||||
<AppText>{labels.deleteSegment.explainer}</AppText>
|
isDeletingData={isDeletingData}
|
||||||
<DeleteData
|
onStartDeletion={() => setIsDeletingData(true)}
|
||||||
isDeletingData={isDeletingData}
|
/>
|
||||||
onStartDeletion={() => setIsDeletingData(true)}
|
|
||||||
/>
|
|
||||||
</Segment>
|
|
||||||
</AppPage>
|
</AppPage>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-14
@@ -11,9 +11,10 @@ import alertError from '../common/alert-error'
|
|||||||
import { clearDb, isDbEmpty } from '../../../db'
|
import { clearDb, isDbEmpty } from '../../../db'
|
||||||
import { showToast } from '../../helpers/general'
|
import { showToast } from '../../helpers/general'
|
||||||
import { hasEncryptionObservable } from '../../../local-storage'
|
import { hasEncryptionObservable } from '../../../local-storage'
|
||||||
import settings from '../../../i18n/en/settings'
|
|
||||||
import { shared as sharedLabels } from '../../../i18n/en/labels'
|
|
||||||
import { EXPORT_FILE_NAME } from './constants'
|
import { EXPORT_FILE_NAME } from './constants'
|
||||||
|
import Segment from '../../common/segment'
|
||||||
|
import AppText from '../../common/app-text'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const exportedFilePath = `${RNFS.DocumentDirectoryPath}/${EXPORT_FILE_NAME}`
|
const exportedFilePath = `${RNFS.DocumentDirectoryPath}/${EXPORT_FILE_NAME}`
|
||||||
|
|
||||||
@@ -22,6 +23,10 @@ const DeleteData = ({ onStartDeletion, isDeletingData }) => {
|
|||||||
const [isConfirmingWithPassword, setIsConfirmingWithPassword] =
|
const [isConfirmingWithPassword, setIsConfirmingWithPassword] =
|
||||||
useState(false)
|
useState(false)
|
||||||
|
|
||||||
|
const { t } = useTranslation(null, {
|
||||||
|
keyPrefix: 'hamburgerMenu.settings.data.delete',
|
||||||
|
})
|
||||||
|
|
||||||
const onAlertConfirmation = () => {
|
const onAlertConfirmation = () => {
|
||||||
onStartDeletion()
|
onStartDeletion()
|
||||||
if (isPasswordSet) {
|
if (isPasswordSet) {
|
||||||
@@ -32,17 +37,16 @@ const DeleteData = ({ onStartDeletion, isDeletingData }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const alertBeforeDeletion = async () => {
|
const alertBeforeDeletion = async () => {
|
||||||
const { question, message, confirmation, errors } = settings.deleteSegment
|
|
||||||
if (isDbEmpty() && !(await RNFS.exists(exportedFilePath))) {
|
if (isDbEmpty() && !(await RNFS.exists(exportedFilePath))) {
|
||||||
alertError(errors.noData)
|
alertError(t('error.noData'))
|
||||||
} else {
|
} else {
|
||||||
Alert.alert(question, message, [
|
Alert.alert(t('dialog.title'), t('dialog.message'), [
|
||||||
{
|
{
|
||||||
text: confirmation,
|
text: t('dialog.delete'),
|
||||||
onPress: onAlertConfirmation,
|
onPress: onAlertConfirmation,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: sharedLabels.cancel,
|
text: t('dialog.cancel'),
|
||||||
style: 'cancel',
|
style: 'cancel',
|
||||||
onPress: cancelConfirmationWithPassword,
|
onPress: cancelConfirmationWithPassword,
|
||||||
},
|
},
|
||||||
@@ -57,16 +61,14 @@ const DeleteData = ({ onStartDeletion, isDeletingData }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteAppData = async () => {
|
const deleteAppData = async () => {
|
||||||
const { errors, success } = settings.deleteSegment
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!isDbEmpty()) {
|
if (!isDbEmpty()) {
|
||||||
clearDb()
|
clearDb()
|
||||||
}
|
}
|
||||||
await deleteExportedFile()
|
await deleteExportedFile()
|
||||||
showToast(success.message)
|
showToast(t('success.message'))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alertError(errors.couldNotDeleteFile)
|
alertError(t('error.delete'))
|
||||||
}
|
}
|
||||||
cancelConfirmationWithPassword()
|
cancelConfirmationWithPassword()
|
||||||
}
|
}
|
||||||
@@ -85,9 +87,12 @@ const DeleteData = ({ onStartDeletion, isDeletingData }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button isCTA onPress={alertBeforeDeletion}>
|
<Segment title={t('title')} last>
|
||||||
{settings.deleteSegment.title}
|
<AppText>{t('subTitle')}</AppText>
|
||||||
</Button>
|
<Button isCTA onPress={alertBeforeDeletion}>
|
||||||
|
{t('title')}
|
||||||
|
</Button>
|
||||||
|
</Segment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { getCycleDaysSortedByDate, mapRealmObjToJsObj } from '../../../db'
|
||||||
|
import getDataAsCsvDataUri from '../../../lib/import-export/export-to-csv'
|
||||||
|
import alertError from '../common/alert-error'
|
||||||
|
import { EXPORT_FILE_NAME } from './constants'
|
||||||
|
import RNFS from 'react-native-fs'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import AppText from '../../common/app-text'
|
||||||
|
import Button from '../../common/button'
|
||||||
|
import Segment from '../../common/segment'
|
||||||
|
import Share from 'react-native-share'
|
||||||
|
|
||||||
|
export default function ExportData({ setIsLoading, resetIsDeletingData }) {
|
||||||
|
const { t } = useTranslation(null, {
|
||||||
|
keyPrefix: 'hamburgerMenu.settings.data.export',
|
||||||
|
})
|
||||||
|
|
||||||
|
async function startExport() {
|
||||||
|
resetIsDeletingData()
|
||||||
|
setIsLoading(true)
|
||||||
|
await exportData()
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getData() {
|
||||||
|
const cycleDaysByDate = mapRealmObjToJsObj(getCycleDaysSortedByDate())
|
||||||
|
|
||||||
|
try {
|
||||||
|
return cycleDaysByDate.length
|
||||||
|
? getDataAsCsvDataUri(cycleDaysByDate)
|
||||||
|
: null
|
||||||
|
} catch (err) {
|
||||||
|
alertError(t('error.convert'))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportData() {
|
||||||
|
const data = await getData()
|
||||||
|
if (!data) {
|
||||||
|
alertError(t('error.data'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const path = `${RNFS.DocumentDirectoryPath}/${EXPORT_FILE_NAME}`
|
||||||
|
await RNFS.writeFile(path, data)
|
||||||
|
|
||||||
|
await Share.open({
|
||||||
|
title: t('title'),
|
||||||
|
url: `file://${path}`,
|
||||||
|
subject: t('title'),
|
||||||
|
type: 'text/csv',
|
||||||
|
showAppsToView: true,
|
||||||
|
failOnCancel: false,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
return alertError(t('error.share'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment title={t('button')}>
|
||||||
|
<AppText>{t('text')}</AppText>
|
||||||
|
<Button isCTA onPress={startExport}>
|
||||||
|
{t('button')}
|
||||||
|
</Button>
|
||||||
|
</Segment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportData.propTypes = {
|
||||||
|
resetIsDeletingData: PropTypes.func.isRequired,
|
||||||
|
setIsLoading: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ export default function ImportData({ resetIsDeletingData, setIsLoading }) {
|
|||||||
const fileContent = await rnfs.readFile(fileInfo.uri, 'utf8')
|
const fileContent = await rnfs.readFile(fileInfo.uri, 'utf8')
|
||||||
return fileContent
|
return fileContent
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return showImportErrorAlert(t('error.couldNotOpenFile'))
|
return showImportErrorAlert(t('errors.couldNotOpenFile'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ export default function ImportData({ resetIsDeletingData, setIsLoading }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showImportErrorAlert(message) {
|
function showImportErrorAlert(message) {
|
||||||
const errorMessage = t('error.noDataImported', { message })
|
const errorMessage = t('errors.noDataImported', { message })
|
||||||
alertError(errorMessage)
|
alertError(errorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import Share from 'react-native-share'
|
|
||||||
|
|
||||||
import { getCycleDaysSortedByDate, mapRealmObjToJsObj } from '../../../db'
|
|
||||||
import getDataAsCsvDataUri from '../../../lib/import-export/export-to-csv'
|
|
||||||
import alertError from '../common/alert-error'
|
|
||||||
import settings from '../../../i18n/en/settings'
|
|
||||||
import { EXPORT_FILE_NAME } from './constants'
|
|
||||||
import RNFS from 'react-native-fs'
|
|
||||||
|
|
||||||
export default async function exportData() {
|
|
||||||
let data
|
|
||||||
const labels = settings.export
|
|
||||||
const cycleDaysByDate = mapRealmObjToJsObj(getCycleDaysSortedByDate())
|
|
||||||
|
|
||||||
if (!cycleDaysByDate.length) return alertError(labels.errors.noData)
|
|
||||||
|
|
||||||
try {
|
|
||||||
data = getDataAsCsvDataUri(cycleDaysByDate)
|
|
||||||
if (!data) {
|
|
||||||
return alertError(labels.errors.noData)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
return alertError(labels.errors.couldNotConvert)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const path = `${RNFS.DocumentDirectoryPath}/${EXPORT_FILE_NAME}`
|
|
||||||
await RNFS.writeFile(path, data)
|
|
||||||
|
|
||||||
await Share.open({
|
|
||||||
title: labels.title,
|
|
||||||
url: `file://${path}`,
|
|
||||||
subject: labels.subject,
|
|
||||||
type: 'text/csv',
|
|
||||||
showAppsToView: true,
|
|
||||||
failOnCancel: false,
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
return alertError(labels.errors.problemSharing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+28
-10
@@ -1,12 +1,4 @@
|
|||||||
{
|
{
|
||||||
"bottomMenu": {
|
|
||||||
"calendar": "Calendar",
|
|
||||||
"chart": "Chart",
|
|
||||||
"stats": "Stats"
|
|
||||||
},
|
|
||||||
"chart": {
|
|
||||||
"tutorial": "You can swipe the chart to view more dates."
|
|
||||||
},
|
|
||||||
"cycleDay": {
|
"cycleDay": {
|
||||||
"symptomBox": {
|
"symptomBox": {
|
||||||
"bleeding": "Bleeding",
|
"bleeding": "Bleeding",
|
||||||
@@ -89,6 +81,33 @@
|
|||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"data": {
|
"data": {
|
||||||
|
"delete": {
|
||||||
|
"dialog": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"delete": "Delete app data permanently",
|
||||||
|
"message": "Please note that deletion of the app data is permanent and irreversible. We recommend exporting existing data before deletion.",
|
||||||
|
"title": "Do you want to delete app data from this phone?"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"delete": "Could not delete data",
|
||||||
|
"noData": "There is no data to delete"
|
||||||
|
},
|
||||||
|
"subTitle": "Delete app data from this phone",
|
||||||
|
"success": {
|
||||||
|
"message": "App data successfully deleted"
|
||||||
|
},
|
||||||
|
"title": "Delete app data"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"button": "Export data",
|
||||||
|
"error": {
|
||||||
|
"convert": "Could not convert data to CSV",
|
||||||
|
"data": "There is no data to export",
|
||||||
|
"share": "There was a problem sharing the data export file"
|
||||||
|
},
|
||||||
|
"text": "Export data in CSV format for backup or so you can use it elsewhere",
|
||||||
|
"title": "My drip. data export"
|
||||||
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"button": "Import data",
|
"button": "Import data",
|
||||||
"dialog": {
|
"dialog": {
|
||||||
@@ -98,10 +117,9 @@
|
|||||||
"replace": "Import and replace",
|
"replace": "Import and replace",
|
||||||
"title": "Keep existing data?"
|
"title": "Keep existing data?"
|
||||||
},
|
},
|
||||||
"error": {
|
"errors": {
|
||||||
"couldNotOpenFile": "Could not open file",
|
"couldNotOpenFile": "Could not open file",
|
||||||
"futureEdit": "Future dates may only contain a note, no other symptoms",
|
"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"
|
"noDataImported": "{{message}}\n\nNo data was imported or changed"
|
||||||
},
|
},
|
||||||
"segmentExplainer": "Import data in CSV format",
|
"segmentExplainer": "Import data in CSV format",
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ export const home = {
|
|||||||
phase: (n) => `${['1st', '2nd', '3rd'][n - 1]} cycle phase`,
|
phase: (n) => `${['1st', '2nd', '3rd'][n - 1]} cycle phase`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const chart = {
|
||||||
|
tutorial: 'You can swipe the chart to view more dates.',
|
||||||
|
}
|
||||||
|
|
||||||
export const shared = {
|
export const shared = {
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
|
|||||||
+14
-24
@@ -1,32 +1,22 @@
|
|||||||
import links from './links'
|
import links from './links'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
export: {
|
menuItems: {
|
||||||
errors: {
|
reminders: {
|
||||||
noData: 'There is no data to export',
|
name: 'Reminders',
|
||||||
couldNotConvert: 'Could not convert data to CSV',
|
text: 'turn on/off reminders',
|
||||||
problemSharing: 'There was a problem sharing the data export file',
|
|
||||||
},
|
},
|
||||||
title: 'My drip. data export',
|
nfpSettings: {
|
||||||
subject: 'My drip. data export',
|
name: 'NFP settings',
|
||||||
button: 'Export data',
|
text: 'define how you want to use NFP',
|
||||||
segmentExplainer:
|
|
||||||
'Export data in CSV format for backup or so you can use it elsewhere',
|
|
||||||
},
|
|
||||||
deleteSegment: {
|
|
||||||
title: 'Delete app data',
|
|
||||||
explainer: 'Delete app data from this phone',
|
|
||||||
question: 'Do you want to delete app data from this phone?',
|
|
||||||
message:
|
|
||||||
'Please note that deletion of the app data is permanent and irreversible. We recommend exporting existing data before deletion.',
|
|
||||||
confirmation: 'Delete app data permanently',
|
|
||||||
errors: {
|
|
||||||
couldNotDeleteFile: 'Could not delete data',
|
|
||||||
postFix: 'No data was deleted or changed',
|
|
||||||
noData: 'There is no data to delete',
|
|
||||||
},
|
},
|
||||||
success: {
|
dataManagement: {
|
||||||
message: 'App data successfully deleted',
|
name: 'Data',
|
||||||
|
text: 'import, export or delete your data',
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
name: 'Password',
|
||||||
|
text: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tempScale: {
|
tempScale: {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ i18n
|
|||||||
compatibilityJSON: 'v3', // TODO: migrate json to v4 and afterwards remove it
|
compatibilityJSON: 'v3', // TODO: migrate json to v4 and afterwards remove it
|
||||||
resources,
|
resources,
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
|
debug: true,
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false, // not needed for react as it escapes by default
|
escapeValue: false, // not needed for react as it escapes by default
|
||||||
|
|||||||
@@ -5,8 +5,4 @@ module.exports = {
|
|||||||
transformIgnorePatterns: [
|
transformIgnorePatterns: [
|
||||||
'node_modules/(?!((jest-)?react-native(-.*)?|@react-native(-community)?)/)',
|
'node_modules/(?!((jest-)?react-native(-.*)?|@react-native(-community)?)/)',
|
||||||
],
|
],
|
||||||
watchPlugins: [
|
|
||||||
'jest-watch-typeahead/filename',
|
|
||||||
'jest-watch-typeahead/testname',
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import getColumnNamesForCsv from './get-csv-column-names'
|
import getColumnNamesForCsv from './get-csv-column-names'
|
||||||
import replaceWithNullIfAllPropertiesAreNull from './replace-with-null'
|
import replaceWithNullIfAllPropertiesAreNull from './replace-with-null'
|
||||||
import { LocalDate } from '@js-joda/core'
|
import { LocalDate } from '@js-joda/core'
|
||||||
import i18next from 'i18next'
|
import labels from '../../i18n/en/settings'
|
||||||
|
|
||||||
export default async function importCsv(csv, deleteFirst) {
|
export default async function importCsv(csv, deleteFirst) {
|
||||||
const parseFuncs = {
|
const parseFuncs = {
|
||||||
@@ -46,10 +46,7 @@ export default async function importCsv(csv, deleteFirst) {
|
|||||||
|
|
||||||
const cycleDays = await csvParser(config)
|
const cycleDays = await csvParser(config)
|
||||||
.fromString(csv)
|
.fromString(csv)
|
||||||
.on('header', (headers) => validateHeaders(headers))
|
.on('header', validateHeaders)
|
||||||
.on('error', (error) => {
|
|
||||||
throw error
|
|
||||||
})
|
|
||||||
|
|
||||||
//remove symptoms where all fields are null
|
//remove symptoms where all fields are null
|
||||||
putNullForEmptySymptoms(cycleDays)
|
putNullForEmptySymptoms(cycleDays)
|
||||||
@@ -70,11 +67,8 @@ function validateHeaders(headers) {
|
|||||||
return expectedHeaders.indexOf(header) > -1
|
return expectedHeaders.indexOf(header) > -1
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
const msg = `Expected CSV column titles to be ${expectedHeaders.join()}`
|
||||||
i18next.t('hamburgerMenu.settings.data.import.error.incorrectColumns', {
|
throw new Error(msg)
|
||||||
incorrectColumns: expectedHeaders.join(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,9 +92,7 @@ function throwIfFutureData(cycleDays) {
|
|||||||
day.date > today &&
|
day.date > today &&
|
||||||
Object.keys(day).some((symptom) => symptom != 'date' && symptom != 'note')
|
Object.keys(day).some((symptom) => symptom != 'date' && symptom != 'note')
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(labels.import.errors.futureEdit)
|
||||||
i18next.t('hamburgerMenu.settings.data.import.error.futureEdit')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -1,16 +1,17 @@
|
|||||||
export default function getSensiplanMucus(feeling, texture) {
|
export default function (feeling, texture) {
|
||||||
|
|
||||||
if (typeof feeling != 'number' || typeof texture != 'number') return null
|
if (typeof feeling != 'number' || typeof texture != 'number') return null
|
||||||
|
|
||||||
const feelingMapping = {
|
const feelingMapping = {
|
||||||
0: 0,
|
0: 0,
|
||||||
1: 1,
|
1: 1,
|
||||||
2: 2,
|
2: 2,
|
||||||
3: 4,
|
3: 4
|
||||||
}
|
}
|
||||||
const textureMapping = {
|
const textureMapping = {
|
||||||
0: 0,
|
0: 0,
|
||||||
1: 3,
|
1: 3,
|
||||||
2: 4,
|
2: 4
|
||||||
}
|
}
|
||||||
const nfpFeelingValue = feelingMapping[feeling]
|
const nfpFeelingValue = feelingMapping[feeling]
|
||||||
const nfpTextureValue = textureMapping[texture]
|
const nfpTextureValue = textureMapping[texture]
|
||||||
|
|||||||
+7
-8
@@ -36,14 +36,14 @@
|
|||||||
"@react-native-community/datetimepicker": "^6.3.1",
|
"@react-native-community/datetimepicker": "^6.3.1",
|
||||||
"@react-native-community/push-notification-ios": "^1.8.0",
|
"@react-native-community/push-notification-ios": "^1.8.0",
|
||||||
"csvtojson": "^2.0.8",
|
"csvtojson": "^2.0.8",
|
||||||
"i18next": "^22.4.13",
|
"i18next": "^21.9.0",
|
||||||
"jshashes": "^1.0.8",
|
"jshashes": "^1.0.8",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"object-path": "^0.11.4",
|
"object-path": "^0.11.4",
|
||||||
"obv": "0.0.1",
|
"obv": "0.0.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-i18next": "^12.0.0",
|
"react-i18next": "^11.18.3",
|
||||||
"react-native": "0.67.4",
|
"react-native": "0.67.4",
|
||||||
"react-native-calendars": "^1.1287.0",
|
"react-native-calendars": "^1.1287.0",
|
||||||
"react-native-document-picker": "^8.1.1",
|
"react-native-document-picker": "^8.1.1",
|
||||||
@@ -58,18 +58,17 @@
|
|||||||
"sympto": "3.0.1"
|
"sympto": "3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.2",
|
"@babel/core": "^7.12.9",
|
||||||
"@babel/eslint-parser": "^7.19.1",
|
"@babel/eslint-parser": "^7.19.1",
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.16.0",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@testing-library/jest-native": "^4.0.12",
|
"@testing-library/jest-native": "^4.0.12",
|
||||||
"@testing-library/react-native": "^11.1.0",
|
"@testing-library/react-native": "^11.1.0",
|
||||||
"basic-changelog": "gitlab:bloodyhealth/basic-changelog",
|
"basic-changelog": "gitlab:bloodyhealth/basic-changelog",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "7.14.0",
|
||||||
"eslint-plugin-react": "^7.31.10",
|
"eslint-plugin-react": "^7.8.2",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
"jest": "^29.1.2",
|
"jest": "^28.1.3",
|
||||||
"jest-watch-typeahead": "^2.2.0",
|
|
||||||
"jetifier": "^1.6.6",
|
"jetifier": "^1.6.6",
|
||||||
"metro-react-native-babel-preset": "^0.66.2",
|
"metro-react-native-babel-preset": "^0.66.2",
|
||||||
"prettier": "2.4.0",
|
"prettier": "2.4.0",
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react-native'
|
||||||
import AcceptLicense from '../components/AcceptLicense'
|
import AcceptLicense from '../components/AcceptLicense'
|
||||||
|
|
||||||
import { saveLicenseFlag } from '../local-storage'
|
import { saveLicenseFlag } from '../local-storage'
|
||||||
import { render, screen, fireEvent } from './test-utils'
|
|
||||||
|
|
||||||
jest.mock('../local-storage', () => ({
|
jest.mock('../local-storage', () => ({
|
||||||
saveLicenseFlag: jest.fn(() => Promise.resolve()),
|
saveLicenseFlag: jest.fn(() => Promise.resolve()),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
jest.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (str, options) => {
|
||||||
|
return str + (options ? JSON.stringify(options) : '')
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
describe('AcceptLicense', () => {
|
describe('AcceptLicense', () => {
|
||||||
test('should accept license when clicking ok button', async () => {
|
test('On clicking OK button, the license is accepted', async () => {
|
||||||
const mockedSetLicense = jest.fn()
|
const mockedSetLicense = jest.fn()
|
||||||
render(<AcceptLicense setLicense={mockedSetLicense} />)
|
render(<AcceptLicense setLicense={mockedSetLicense} />)
|
||||||
|
|
||||||
const okButton = screen.getByText('OK')
|
const okButton = screen.getByText('ok', { exact: false })
|
||||||
|
|
||||||
fireEvent(okButton, 'click')
|
fireEvent(okButton, 'click')
|
||||||
|
|
||||||
@@ -21,9 +29,9 @@ describe('AcceptLicense', () => {
|
|||||||
expect(mockedSetLicense).toHaveBeenCalled()
|
expect(mockedSetLicense).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should render cancel button', async () => {
|
test('There is a Cancel button', async () => {
|
||||||
render(<AcceptLicense setLicense={jest.fn()} />)
|
render(<AcceptLicense setLicense={jest.fn()} />)
|
||||||
|
|
||||||
screen.getByText('Cancel')
|
screen.getByText('cancel', { exact: false })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+11
-3
@@ -1,16 +1,24 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { render, screen } from '@testing-library/react-native'
|
||||||
import License from '../components/settings/License'
|
import License from '../components/settings/License'
|
||||||
import { render, screen } from './test-utils'
|
|
||||||
|
jest.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (str, options) => {
|
||||||
|
return str + (options ? JSON.stringify(options) : '')
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
describe('License screen', () => {
|
describe('License screen', () => {
|
||||||
test('should display license text with correct year', async () => {
|
test('It should have a correct year', async () => {
|
||||||
render(<License />)
|
render(<License />)
|
||||||
const year = new Date().getFullYear().toString()
|
const year = new Date().getFullYear().toString()
|
||||||
|
|
||||||
screen.getByText(year, { exact: false })
|
screen.getByText(year, { exact: false })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should match the snapshot', async () => {
|
test('It should match the snapshot', async () => {
|
||||||
const licenseScreen = render(<License />)
|
const licenseScreen = render(<License />)
|
||||||
|
|
||||||
expect(licenseScreen).toMatchSnapshot()
|
expect(licenseScreen).toMatchSnapshot()
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`License screen should match the snapshot 1`] = `
|
exports[`License screen It should match the snapshot 1`] = `
|
||||||
<View
|
<View
|
||||||
style={
|
style={
|
||||||
{
|
Object {
|
||||||
"backgroundColor": "#E9F2ED",
|
"backgroundColor": "#E9F2ED",
|
||||||
"flex": 1,
|
"flex": 1,
|
||||||
}
|
}
|
||||||
@@ -11,8 +11,8 @@ exports[`License screen should match the snapshot 1`] = `
|
|||||||
>
|
>
|
||||||
<RCTScrollView
|
<RCTScrollView
|
||||||
contentContainerStyle={
|
contentContainerStyle={
|
||||||
[
|
Array [
|
||||||
{
|
Object {
|
||||||
"backgroundColor": "#E9F2ED",
|
"backgroundColor": "#E9F2ED",
|
||||||
"flexGrow": 1,
|
"flexGrow": 1,
|
||||||
},
|
},
|
||||||
@@ -23,13 +23,13 @@ exports[`License screen should match the snapshot 1`] = `
|
|||||||
<View>
|
<View>
|
||||||
<Text
|
<Text
|
||||||
style={
|
style={
|
||||||
[
|
Array [
|
||||||
{
|
Object {
|
||||||
"color": "#555",
|
"color": "#555",
|
||||||
"fontFamily": "Jost-Book",
|
"fontFamily": "Jost-Book",
|
||||||
"fontSize": 34.285714285714285,
|
"fontSize": 34.285714285714285,
|
||||||
},
|
},
|
||||||
{
|
Object {
|
||||||
"alignSelf": "center",
|
"alignSelf": "center",
|
||||||
"color": "#3A2671",
|
"color": "#3A2671",
|
||||||
"fontFamily": "Jost-Bold",
|
"fontFamily": "Jost-Bold",
|
||||||
@@ -41,11 +41,11 @@ exports[`License screen should match the snapshot 1`] = `
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
drip. an open-source cycle tracking app
|
title
|
||||||
</Text>
|
</Text>
|
||||||
<View
|
<View
|
||||||
style={
|
style={
|
||||||
{
|
Object {
|
||||||
"marginBottom": 34.285714285714285,
|
"marginBottom": 34.285714285714285,
|
||||||
"marginHorizontal": 34.285714285714285,
|
"marginHorizontal": 34.285714285714285,
|
||||||
}
|
}
|
||||||
@@ -53,8 +53,8 @@ exports[`License screen should match the snapshot 1`] = `
|
|||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={
|
style={
|
||||||
[
|
Array [
|
||||||
{
|
Object {
|
||||||
"color": "#555",
|
"color": "#555",
|
||||||
"fontFamily": "Jost-Book",
|
"fontFamily": "Jost-Book",
|
||||||
"fontSize": 34.285714285714285,
|
"fontSize": 34.285714285714285,
|
||||||
@@ -63,14 +63,12 @@ exports[`License screen should match the snapshot 1`] = `
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Copyright (C) 2023 Heart of Code e.V.
|
text{"currentYear":2022}
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details:
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
onPress={[Function]}
|
onPress={[Function]}
|
||||||
style={
|
style={
|
||||||
{
|
Object {
|
||||||
"color": "#3A2671",
|
"color": "#3A2671",
|
||||||
"fontFamily": "Jost-Book",
|
"fontFamily": "Jost-Book",
|
||||||
"fontSize": 34.285714285714285,
|
"fontSize": 34.285714285714285,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
exports[`Footnote component when children are present, renders them 1`] = `
|
exports[`Footnote component when children are present, renders them 1`] = `
|
||||||
<View
|
<View
|
||||||
style={
|
style={
|
||||||
{
|
Object {
|
||||||
"alignContent": "flex-start",
|
"alignContent": "flex-start",
|
||||||
"flexDirection": "row",
|
"flexDirection": "row",
|
||||||
"marginBottom": 8.571428571428571,
|
"marginBottom": 8.571428571428571,
|
||||||
@@ -13,13 +13,13 @@ exports[`Footnote component when children are present, renders them 1`] = `
|
|||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={
|
style={
|
||||||
[
|
Array [
|
||||||
{
|
Object {
|
||||||
"color": "#555",
|
"color": "#555",
|
||||||
"fontFamily": "Jost-Book",
|
"fontFamily": "Jost-Book",
|
||||||
"fontSize": 34.285714285714285,
|
"fontSize": 34.285714285714285,
|
||||||
},
|
},
|
||||||
{
|
Object {
|
||||||
"color": "#F38337",
|
"color": "#F38337",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -29,18 +29,18 @@ exports[`Footnote component when children are present, renders them 1`] = `
|
|||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
linkStyle={
|
linkStyle={
|
||||||
{
|
Object {
|
||||||
"color": "white",
|
"color": "white",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
[
|
Array [
|
||||||
{
|
Object {
|
||||||
"color": "#555",
|
"color": "#555",
|
||||||
"fontFamily": "Jost-Book",
|
"fontFamily": "Jost-Book",
|
||||||
"fontSize": 34.285714285714285,
|
"fontSize": 34.285714285714285,
|
||||||
},
|
},
|
||||||
{
|
Object {
|
||||||
"color": "#555",
|
"color": "#555",
|
||||||
"paddingLeft": 21.428571428571427,
|
"paddingLeft": 21.428571428571427,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { render } from '@testing-library/react-native'
|
|
||||||
import '../i18n/i18n'
|
|
||||||
|
|
||||||
const customRender = (ui, options) => render(ui, { ...options })
|
|
||||||
|
|
||||||
// re-export everything
|
|
||||||
export * from '@testing-library/react-native'
|
|
||||||
|
|
||||||
// override render method
|
|
||||||
export { customRender as render }
|
|
||||||
Reference in New Issue
Block a user