diff --git a/www/__tests__/commHelper.test.ts b/www/__tests__/commHelper.test.ts index d7018abb5..11e76bec0 100644 --- a/www/__tests__/commHelper.test.ts +++ b/www/__tests__/commHelper.test.ts @@ -47,4 +47,6 @@ it('fetches text from a URL and caches it so the next call is faster', async () * - updateUser * - getUser * - putOne + * - getModes + * - updateMode */ diff --git a/www/i18n/en.json b/www/i18n/en.json index aa41988f3..6f87f6c5e 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -8,7 +8,9 @@ "trip-confirm": { "services-please-fill-in": "Please fill in the {{text}} not listed.", "services-cancel": "Cancel", - "services-save": "Save" + "services-save": "Save", + "custom-mode": "Custom Mode", + "custom-purpose": "Custom Purpose" }, "control": { diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index 946ccb91c..128671c7a 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -26,7 +26,7 @@ import { fillLocationNamesOfTrip, resetNominatimLimiter } from './addressNamesHe import { getLabelOptions, labelOptionByValue } from '../survey/multilabel/confirmHelper'; import { displayError, displayErrorMsg, logDebug, logWarn } from '../plugin/logger'; import { useTheme } from 'react-native-paper'; -import { getPipelineRangeTs } from '../services/commHelper'; +import { getPipelineRangeTs, getUserCustomLabels } from '../services/commHelper'; import { getNotDeletedCandidates, mapInputsToTimelineEntries } from '../survey/inputMatcher'; import { configuredFilters as multilabelConfiguredFilters } from '../survey/multilabel/infinite_scroll_filters'; import { configuredFilters as enketoConfiguredFilters } from '../survey/enketo/infinite_scroll_filters'; @@ -34,6 +34,7 @@ import LabelTabContext, { TimelineLabelMap, TimelineMap, TimelineNotesMap, + CustomLabelMap, } from './LabelTabContext'; import { readAllCompositeTrips, readUnprocessedTrips } from './timelineHelper'; import { LabelOptions, MultilabelKey } from '../types/labelTypes'; @@ -42,6 +43,7 @@ import { CompositeTrip, TimelineEntry, TimestampRange, UserInputEntry } from '.. let showPlaces; const ONE_DAY = 24 * 60 * 60; // seconds const ONE_WEEK = ONE_DAY * 7; // seconds +const CUSTOM_LABEL_KEYS_IN_DATABASE = ['mode', 'purpose']; const LabelTab = () => { const appConfig = useAppConfig(); @@ -59,6 +61,7 @@ const LabelTab = () => { const [displayedEntries, setDisplayedEntries] = useState(null); const [refreshTime, setRefreshTime] = useState(null); const [isLoading, setIsLoading] = useState('replace'); + const [customLabelMap, setCustomLabelMap] = useState({}); // initialization, once the appConfig is loaded useEffect(() => { @@ -66,7 +69,7 @@ const LabelTab = () => { if (!appConfig) return; showPlaces = appConfig.survey_info?.buttons?.['place-notes']; getLabelOptions(appConfig).then((labelOptions) => setLabelOptions(labelOptions)); - + getUserCustomLabels(CUSTOM_LABEL_KEYS_IN_DATABASE).then((res) => setCustomLabelMap(res)); // we will show filters if 'additions' are not configured // https://github.com/e-mission/e-mission-docs/issues/894 if (appConfig.survey_info?.buttons == undefined) { @@ -360,6 +363,8 @@ const LabelTab = () => { loadAnotherWeek, loadSpecificWeek, refresh, + customLabelMap, + setCustomLabelMap, }; const Tab = createStackNavigator(); diff --git a/www/js/diary/LabelTabContext.ts b/www/js/diary/LabelTabContext.ts index 89448082b..3da7f31a4 100644 --- a/www/js/diary/LabelTabContext.ts +++ b/www/js/diary/LabelTabContext.ts @@ -1,4 +1,4 @@ -import { createContext } from 'react'; +import { Dispatch, SetStateAction, createContext } from 'react'; import { TimelineEntry, UserInputEntry } from '../types/diaryTypes'; import { LabelOption, LabelOptions, MultilabelKey } from '../types/labelTypes'; import { EnketoUserInputEntry } from '../survey/enketo/enketoHelper'; @@ -20,6 +20,9 @@ export type TimelineLabelMap = { export type TimelineNotesMap = { [k: string]: UserInputEntry[]; }; +export type CustomLabelMap = { + [k: string]: string[]; +}; type ContextProps = { labelOptions: LabelOptions | null; @@ -37,6 +40,8 @@ type ContextProps = { loadAnotherWeek: any; // TODO loadSpecificWeek: any; // TODO refresh: any; // TODO + customLabelMap: CustomLabelMap; + setCustomLabelMap: Dispatch>; }; export default createContext({} as ContextProps); diff --git a/www/js/services/commHelper.ts b/www/js/services/commHelper.ts index 26dce8056..d17a7e621 100644 --- a/www/js/services/commHelper.ts +++ b/www/js/services/commHelper.ts @@ -227,3 +227,39 @@ export function putOne(key, data) { throw error; }); } + +export function getUserCustomLabels(keys) { + return new Promise((rs, rj) => { + window['cordova'].plugins.BEMServerComm.postUserPersonalData( + '/customlabel/get', + 'keys', + keys, + rs, + rj, + ); + }).catch((error) => { + error = 'While getting labels, ' + error; + throw error; + }); +} + +export function updateUserCustomLabel(key, oldLabel, newLabel, isNewLabelMustAdded) { + const updatedLabel = { + key: key, + old_label: oldLabel, + new_label: newLabel, + is_new_label_must_added: isNewLabelMustAdded, + }; + return new Promise((rs, rj) => { + window['cordova'].plugins.BEMServerComm.postUserPersonalData( + '/customlabel/update', + 'updated_label', + updatedLabel, + rs, + rj, + ); + }).catch((error) => { + error = 'While putting one label, ' + error; + throw error; + }); +} diff --git a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx index 517223141..6437f93f5 100644 --- a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx +++ b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx @@ -12,6 +12,7 @@ import { RadioButton, Button, TextInput, + Divider, } from 'react-native-paper'; import DiaryButton from '../../components/DiaryButton'; import { useTranslation } from 'react-i18next'; @@ -29,23 +30,28 @@ import { } from './confirmHelper'; import useAppConfig from '../../useAppConfig'; import { MultilabelKey } from '../../types/labelTypes'; +import { updateUserCustomLabel } from '../../services/commHelper'; const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => { const { colors } = useTheme(); const { t } = useTranslation(); const appConfig = useAppConfig(); - const { labelOptions, labelFor, userInputFor, addUserInputToEntry } = useContext(LabelTabContext); + const { + labelOptions, + labelFor, + userInputFor, + addUserInputToEntry, + customLabelMap, + setCustomLabelMap, + } = useContext(LabelTabContext); const { height: windowHeight } = useWindowDimensions(); - // modal visible for which input type? (MODE or PURPOSE or REPLACED_MODE, null if not visible) const [modalVisibleFor, setModalVisibleFor] = useState(null); const [otherLabel, setOtherLabel] = useState(null); - const chosenLabel = useMemo(() => { + const initialLabel = useMemo(() => { if (modalVisibleFor == null) return null; - if (otherLabel != null) return 'other'; return labelFor(trip, modalVisibleFor)?.value || null; - }, [modalVisibleFor, otherLabel]); - + }, [modalVisibleFor]); // to mark 'inferred' labels as 'confirmed'; turn yellow labels blue function verifyTrip() { const inferredLabelsForTrip = inferFinalLabels(trip, userInputFor(trip)); @@ -81,16 +87,35 @@ const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => { if (!Object.keys(inputs).length) return displayErrorMsg('No inputs to store'); const inputsToStore: UserInputMap = {}; const storePromises: any[] = []; - for (let [inputType, chosenLabel] of Object.entries(inputs)) { + for (let [inputType, newLabel] of Object.entries(inputs)) { if (isOther) { /* Let's make the value for user entered inputs look consistent with our other values (i.e. lowercase, and with underscores instead of spaces) */ - chosenLabel = readableLabelToKey(chosenLabel); + newLabel = readableLabelToKey(newLabel); + } + // If a user saves a new customized label or makes changes to/from customized labels, the labels need to be updated. + const key = inputType.toLowerCase(); + if ( + isOther || + (initialLabel && customLabelMap[key].indexOf(initialLabel) > -1) || + (newLabel && customLabelMap[key].indexOf(newLabel) > -1) + ) { + updateUserCustomLabel(key, initialLabel ?? '', newLabel, isOther) + .then((res) => { + setCustomLabelMap({ + ...customLabelMap, + [key]: res['label'], + }); + logDebug('Successfuly stored custom label ' + JSON.stringify(res)); + }) + .catch((e) => { + displayErrorMsg(e, 'Create Label Error'); + }); } const inputDataToStore = { start_ts: trip.start_ts, end_ts: trip.end_ts, - label: chosenLabel, + label: newLabel, }; inputsToStore[inputType] = inputDataToStore; @@ -107,6 +132,8 @@ const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => { } const tripInputDetails = labelInputDetailsForTrip(userInputFor(trip), appConfig); + const customLabelKeyInDatabase = modalVisibleFor === 'PURPOSE' ? 'purpose' : 'mode'; + return ( <> @@ -164,16 +191,47 @@ const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => { onChooseLabel(val)} - value={chosenLabel || ''}> + // if 'other' button is selected and input component shows up, make 'other' radio button filled + value={otherLabel !== null ? 'other' : initialLabel || ''}> {modalVisibleFor && - labelOptions?.[modalVisibleFor]?.map((o, i) => ( - - ))} + labelOptions?.[modalVisibleFor]?.map((o, i) => { + const radioItemForOption = ( + + ); + /* if this is the 'other' option and there are some custom labels, + show the custom labels section before 'other' */ + if (o.value == 'other' && customLabelMap[customLabelKeyInDatabase]?.length) { + return ( + <> + + + {(modalVisibleFor === 'MODE' || + modalVisibleFor === 'REPLACED_MODE') && + t('trip-confirm.custom-mode')} + {modalVisibleFor === 'PURPOSE' && t('trip-confirm.custom-purpose')} + + {customLabelMap[customLabelKeyInDatabase].map((key, i) => ( + + ))} + + {radioItemForOption} + + ); + } + // otherwise, just show the radio item as normal + return radioItemForOption; + })} @@ -185,6 +243,7 @@ const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => { })} value={otherLabel || ''} onChangeText={(t) => setOtherLabel(t)} + maxLength={25} />