diff --git a/src/client/app/components/ExportComponent.tsx b/src/client/app/components/ExportComponent.tsx index 87711f7ea0..34fedf98ca 100644 --- a/src/client/app/components/ExportComponent.tsx +++ b/src/client/app/components/ExportComponent.tsx @@ -5,11 +5,19 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Button } from 'reactstrap'; +import { useTranslate } from '../redux/componentHooks'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectChartToRender } from '../redux/slices/graphSlice'; import { exportGraphReadingsThunk, exportRawReadings } from '../redux/thunks/exportThunk'; import { ChartTypes } from '../types/redux/graph'; +import ConfirmActionModalComponent from './ConfirmActionModalComponent'; import TooltipMarkerComponent from './TooltipMarkerComponent'; + +interface PendingExportConfirmation { + message: string; + resolve: (confirmed: boolean) => void; +} + /** * Creates export buttons and does code for handling export to CSV files. * @returns HTML for export buttons @@ -17,6 +25,33 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; export default function ExportComponent() { const dispatch = useAppDispatch(); const chartToRender = useAppSelector(selectChartToRender); + const translate = useTranslate(); + const [pendingExportConfirmation, setPendingExportConfirmation] = React.useState(null); + const pendingExportConfirmationRef = React.useRef(null); + + const requestExportConfirmation = React.useCallback((message: string) => new Promise(resolve => { + // Do not replace an unresolved confirmation if the export button is activated more than once. + if (pendingExportConfirmationRef.current !== null) { + resolve(false); + return; + } + const pendingConfirmation = { message, resolve }; + pendingExportConfirmationRef.current = pendingConfirmation; + setPendingExportConfirmation(pendingConfirmation); + }), []); + + const settleExportConfirmation = React.useCallback((confirmed: boolean) => { + const pendingConfirmation = pendingExportConfirmationRef.current; + pendingExportConfirmationRef.current = null; + setPendingExportConfirmation(null); + pendingConfirmation?.resolve(confirmed); + }, []); + + React.useEffect(() => () => { + const pendingConfirmation = pendingExportConfirmationRef.current; + pendingExportConfirmationRef.current = null; + pendingConfirmation?.resolve(false); + }, []); return ( <> @@ -33,11 +68,21 @@ export default function ExportComponent() { /* Only raw export if a line graph */ chartToRender === ChartTypes.line &&
-
} + settleExportConfirmation(false)} + actionFunction={() => settleExportConfirmation(true)} + actionRejectText={translate('cancel')} + actionConfirmText={translate('continue')} + /> ); } diff --git a/src/client/app/components/groups/CreateGroupModalComponent.tsx b/src/client/app/components/groups/CreateGroupModalComponent.tsx index a9dba8517f..e650a06ab6 100644 --- a/src/client/app/components/groups/CreateGroupModalComponent.tsx +++ b/src/client/app/components/groups/CreateGroupModalComponent.tsx @@ -31,6 +31,7 @@ import { AreaUnitType, getAreaUnitConversion } from '../../utils/getAreaUnitConv import { getGPSString } from '../../utils/input'; import { showSuccessNotification, showErrorNotification, showWarnNotification } from '../../utils/notifications'; import { useTranslate } from '../../redux/componentHooks'; +import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; import ListDisplayComponent from '../ListDisplayComponent'; import MultiSelectComponent from '../MultiSelectComponent'; import TooltipHelpComponent from '../TooltipHelpComponent'; @@ -115,6 +116,7 @@ export default function CreateGroupModalComponent() { /* State */ // State for the created group. const [state, setState] = useState(defaultValues); + const [pendingAreaCalculation, setPendingAreaCalculation] = useState<{ message: string; area: number } | null>(null); // Handlers for each type of input change @@ -175,14 +177,18 @@ export default function CreateGroupModalComponent() { notifyMsg += '\n"' + meter.identifier + '"' + translate('group.area.calculate.error.zero'); } }); - let msg = translate('group.area.calculate.header') + areaSum + ' ' + translate(`AreaUnitType.${state.areaUnit}`); + // The + here converts back into a number and removes trailing zeroes. + const roundedArea = +areaSum.toPrecision(6); + let msg = translate('group.area.calculate.confirm') + + roundedArea + ' ' + + translate(`AreaUnitType.${state.areaUnit}`) + '?'; if (notifyMsg != '') { msg += '\n' + translate('group.area.calculate.error.header') + notifyMsg; } - if (window.confirm(msg)) { - // the + here converts back into a number - setState({ ...state, ['area']: + areaSum.toPrecision(6) }); - } + setPendingAreaCalculation({ + message: msg, + area: roundedArea + }); } else { showErrorNotification(translate('group.area.calculate.error.group.unit')); } @@ -191,6 +197,17 @@ export default function CreateGroupModalComponent() { } }; + const handleAreaCalculationConfirm = () => { + if (pendingAreaCalculation !== null) { + setState(currentState => ({ ...currentState, area: pendingAreaCalculation.area })); + } + setPendingAreaCalculation(null); + }; + + const handleAreaCalculationCancel = () => { + setPendingAreaCalculation(null); + }; + const handleClose = () => { setShowModal(false); resetState(); @@ -370,6 +387,15 @@ export default function CreateGroupModalComponent() { disabled={!canSave} /> )} + {/* Show modal button */} ; + const deleteConfirmationMessage = this.props.intl.formatMessage( + { id: 'map.confirm.remove' }, + { name: this.props.map.name } + ); + return <> + + + ; } private styleEnabled(): React.CSSProperties { diff --git a/src/client/app/components/meters/EditMeterModalComponent.tsx b/src/client/app/components/meters/EditMeterModalComponent.tsx index ca7b569e67..31c933eedc 100644 --- a/src/client/app/components/meters/EditMeterModalComponent.tsx +++ b/src/client/app/components/meters/EditMeterModalComponent.tsx @@ -25,6 +25,7 @@ import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import { getGPSString, nullToEmptyString, NoUnit, MIN_VAL, MAX_VAL } from '../../utils/input'; import { showSuccessNotification, showErrorNotification } from '../../utils/notifications'; import { useTranslate } from '../../redux/componentHooks'; +import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; import TimeZoneSelect from '../TimeZoneSelect'; import TooltipHelpComponent from '../TooltipHelpComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; @@ -47,6 +48,10 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr // boolean that updates if any change is made to any meter modal const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [showUnsavedWarning, setShowUnsavedWarning] = useState(false); + const [pendingDisplayableChange, setPendingDisplayableChange] = useState<{ + message: string; + displayable: boolean; + } | null>(null); // If there are no changes, then save is disabled const [canSave, setCanSave] = useState(false); @@ -282,9 +287,8 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr // Function handles the selection of a new displayable. const handleDisplayableChange = (e: React.ChangeEvent) => { - // If there is a potential issue then the admin will decide if save happens. Otherwise, the value is put into state. - let save = true; - if (!JSON.parse(e.target.value)) { + const displayable = JSON.parse(e.target.value); + if (!displayable) { // This will hold the overall message for the admin alert. let msg = ''; // This will hold the names of groups that are affected. @@ -300,12 +304,25 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr // There is a message to display to the user. msg += `${translate('meter')} "${meterState.name}" ${translate('meter.edit.displayable.warning')}\n`; msg += `${groups + '\n' + translate('meter.edit.displayable.verify')}\n`; - save = window.confirm(msg); + setPendingDisplayableChange({ message: msg, displayable }); + return; } } - if (save) { - handleBooleanChange(e); + setLocalMeterEdits({ ...localMeterEdits, displayable }); + }; + + const handleDisplayableChangeConfirm = () => { + if (pendingDisplayableChange !== null) { + setLocalMeterEdits(currentState => ({ + ...currentState, + displayable: pendingDisplayableChange.displayable + })); } + setPendingDisplayableChange(null); + }; + + const handleDisplayableChangeCancel = () => { + setPendingDisplayableChange(null); }; const handleNumberChange = (e: React.ChangeEvent) => { @@ -353,6 +370,15 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr disabled={!canSave || !validMeter} /> )} + diff --git a/src/client/app/redux/thunks/exportThunk.ts b/src/client/app/redux/thunks/exportThunk.ts index 3198fe7a35..49a47ae7b2 100644 --- a/src/client/app/redux/thunks/exportThunk.ts +++ b/src/client/app/redux/thunks/exportThunk.ts @@ -32,6 +32,10 @@ const selectCanExport = (state: RootState) => { return !fetchInProgress && (meterDeps.compatibleEntities.length > 0 || groupDeps.compatibleEntities.length > 0); }; +interface ExportRawReadingsArgs { + requestConfirmation: (message: string) => Promise; +} + export const exportGraphReadingsThunk = createAppThunk( 'graph/exportGraphData', (_unused, api) => { @@ -114,7 +118,7 @@ export const exportGraphReadingsThunk = createAppThunk( export const exportRawReadings = createAppThunk( 'graph/ExportRaw', - async (_arg, api) => { + async ({ requestConfirmation }: ExportRawReadingsArgs, api) => { const state = api.getState(); if (!selectCanExport(state)) { return api.rejectWithValue('Data Fetch In Progress, Or No data'); @@ -150,7 +154,7 @@ export const exportRawReadings = createAppThunk( // A user allowed to do this but need to check okay with them. const msg = translate('csv.download.size.warning.size') + ` ${fileSize.toFixed(2)}MB. ` + translate('csv.download.size.warning.verify') + '?'; - const consent = window.confirm(msg); + const consent = await requestConfirmation(msg); if (consent) { shouldDownload = true; } @@ -164,7 +168,7 @@ export const exportRawReadings = createAppThunk( // Anyone can download if they approve const msg = translate('csv.download.size.warning.size') + ` ${fileSize.toFixed(2)}MB. ` + translate('csv.download.size.warning.verify') + '?'; - const consent = window.confirm(msg); + const consent = await requestConfirmation(msg); if (consent) { shouldDownload = true; } diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index bf9886cd54..3e030af5f8 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -65,6 +65,7 @@ const LocaleTranslationData = { "compare.period": "Compare Period", "compare.raw": "Cannot create comparison graph on raw units such as temperature", "confirm.action": "Confirm Action", + "continue": "Continue", "contact.us": "Contact us", "conversion": "Conversion", "conversion.bidirectional": "Bidirectional:", @@ -227,12 +228,14 @@ const LocaleTranslationData = { "group": "Group", "group.all.meters": "All Meters", "group.area.calculate": "Calculate Group Area", + "group.area.calculate.confirm": "Set the group area to: ", "group.area.calculate.error.group.unit": "No group area unit", "group.area.calculate.error.header": "The following meters were excluded from the sum because:", "group.area.calculate.error.no.meters": "No meters in group", "group.area.calculate.error.unit": ": nonzero area but no area unit", "group.area.calculate.error.zero": ": area is unset or zero", "group.area.calculate.header": "Group Area will be set to ", + "group.area.calculate.update": "Update Area", "group.create.nounit": "The default graphic unit was changed to no unit from ", "group.delete.group": "Delete Group", "group.delete.issue": "is contained in the following groups and cannot be deleted", @@ -245,7 +248,7 @@ const LocaleTranslationData = { "group.edit.empty": "Removing this meter/group means there are no child meters or groups which is not allowed . Delete the group if you want to remove it.", "group.edit.nocompatible": "would have no compatible units by the edit to this group so the edit is cancelled", "group.edit.nounit": "will have its compatible units changed and its default graphic unit set to \"no unit\" by the edit to this group", - "group.edit.verify": "Given the messages, do you want to cancel this change (click Cancel) or continue (click OK)?", + "group.edit.verify": "Given these messages, do you want to continue with this change?", "group.failed.to.create.group": "Failed to create a group with message: ", "group.failed.to.edit.group": "Failed to edit group with message: ", "group.gps.error": "Please input a valid GPS: (latitude, longitude)", @@ -365,7 +368,7 @@ const LocaleTranslationData = { "map.calibrate": "Calibrate", "map.calibration": "Calibration status", "map.circle.size": "Map Circle Size", - "map.confirm.remove": "Are you sure you want to remove map", + "map.confirm.remove": "Are you sure you want to delete the map \"{name}\"?", "map.displayable": "Map Display", "map.filename": "Map file", "map.id": "Map ID", @@ -396,7 +399,7 @@ const LocaleTranslationData = { "meter.cumulativeResetEnd": "Cumulative Reset End:", "meter.cumulativeResetStart": "Cumulative Reset Start:", "meter.disableChecks": "Disable Checks", - "meter.edit.displayable.verify": "Given the group(s) listed above, do you want to cancel this change (click Cancel) or continue (click OK)?", + "meter.edit.displayable.verify": "Given the group(s) listed above, do you want to continue with this change?", "meter.edit.displayable.warning": "is not displayable but is used by the following displayable groups:", "meter.enabled": "Updates:", "meter.endOnlyTime": "Only End Times:", @@ -674,6 +677,7 @@ const LocaleTranslationData = { "compare.period": "Compare Period\u{26A1}", "compare.raw": "Cannot create comparison graph on raw units such as temperature\u{26A1}", "confirm.action": "Confirm Action\u{26A1}", + "continue": "Continuer", "contact.us": "Contactez nous", "conversion": "Conversion\u{26A1}", "conversion.bidirectional": "Bidirectional:\u{26A1}", @@ -836,12 +840,14 @@ const LocaleTranslationData = { "group": "Groupe", "group.all.meters": "Tous les compteurs", "group.area.calculate": "Calculate Group Area\u{26A1}", + "group.area.calculate.confirm": "Définir la surface du groupe à : ", "group.area.calculate.error.group.unit": "No group area unit\u{26A1}", "group.area.calculate.error.header": "The following meters were excluded from the sum because:\u{26A1}", "group.area.calculate.error.no.meters": "No meters in group\u{26A1}", "group.area.calculate.error.unit": ": nonzero area but no area unit\u{26A1}", "group.area.calculate.error.zero": ": area is unset or zero\u{26A1}", "group.area.calculate.header": "Group Area will be set to \u{26A1}", + "group.area.calculate.update": "Mettre à jour la surface", "group.create.nounit": "The default graphic unit was changed to no unit from \u{26A1}", "group.delete.group": "Delete Group\u{26A1}", "group.delete.issue": "is contained in the following groups and cannot be deleted\u{26A1}", @@ -854,7 +860,7 @@ const LocaleTranslationData = { "group.edit.empty": "Removing this meter/group means there are no child meters or groups which is not allowed. Delete the group if you want to remove it.\u{26A1}", "group.edit.nocompatible": "would have no compatible units by the edit to this group so the edit is cancelled\u{26A1}", "group.edit.nounit": "will have its compatible units changed and its default graphic unit set to \"no unit\" by the edit to this group\u{26A1}", - "group.edit.verify": "Given the messages, do you want to cancel this change (click Cancel) or continue (click OK)?\u{26A1}", + "group.edit.verify": "Compte tenu de ces messages, voulez-vous poursuivre cette modification ?", "group.failed.to.create.group": "Failed to create a group with message: \u{26A1}", "group.failed.to.edit.group": "Failed to edit group with message: \u{26A1}", "group.gps.error": "Please input a valid GPS: (latitude, longitude)\u{26A1}", @@ -974,7 +980,7 @@ const LocaleTranslationData = { "map.calibrate": "Étalonner", "map.calibration": "Statut d'étalonnage", "map.circle.size": "Taille du cercle de la carte", - "map.confirm.remove": "Voulez-vous vraiment supprimer la carte?", + "map.confirm.remove": "Voulez-vous vraiment supprimer la carte « {name} » ?", "map.displayable": "Affichage de la carte", "map.filename": "Fichier de carte", "map.id": "ID de la carte", @@ -1005,7 +1011,7 @@ const LocaleTranslationData = { "meter.cumulativeResetEnd": "Cumulative Reset End:\u{26A1}", "meter.cumulativeResetStart": "Cumulative Reset Start:\u{26A1}", "meter.disableChecks": "Disable Checks\u{26A1}", - "meter.edit.displayable.verify": "Given the group(s) listed above, do you want to cancel this change (click Cancel) or continue (click OK)?\u{26A1}", + "meter.edit.displayable.verify": "Compte tenu du ou des groupes indiqués ci-dessus, voulez-vous poursuivre cette modification ?", "meter.edit.displayable.warning": "is not displayable but is used by the following displayable groups:\u{26A1}", "meter.enabled": "Mises à Jour du Mèters", "meter.endOnlyTime": "End Only Time:\u{26A1}", @@ -1283,6 +1289,7 @@ const LocaleTranslationData = { "compare.period": "Compare Period\u{26A1}", "compare.raw": "No se puede crear un gráfico de comparación con unidades crudas como temperatura", "confirm.action": "Confirmar acción", + "continue": "Continuar", "contact.us": "Contáctenos", "conversion": "Conversión", "conversion.bidirectional": "Bidireccional:", @@ -1445,12 +1452,14 @@ const LocaleTranslationData = { "group": "Grupo", "group.all.meters": "Todos los medidores", "group.area.calculate": "Calcular el área del grupo", + "group.area.calculate.confirm": "¿Establecer el área del grupo en: ", "group.area.calculate.error.group.unit": "No hay unidad de área del grupo", "group.area.calculate.error.header": "Los siguientes medidores fueron excluidos de la suma porque:", "group.area.calculate.error.no.meters": "No hay medidores en el grupo", "group.area.calculate.error.unit": ": el área es distinta a cero pero no tiene unidad", "group.area.calculate.error.zero": ": el área no está determinada o es cero", "group.area.calculate.header": "Área del grupo se establecerá en ", + "group.area.calculate.update": "Actualizar área", "group.create.nounit": "La unidad predeterminada del gráfico fue cambiado a sin unidad de ", "group.delete.group": "Borrar grupo", "group.delete.issue": "está en los siguientes grupos y no se puede borrar", @@ -1463,7 +1472,7 @@ const LocaleTranslationData = { "group.edit.empty": "Quitar este medidor/grupo significa que no hay medidores o grupos secundarios, que no está permitido. Borra este grupo si quieres quitarlo.", "group.edit.nocompatible": "no tendría unidades compatibles por la edición a este grupo, por tanto se cancela la edición", "group.edit.nounit": "habrá cambios a sus unidades compatibles y su unidad de gráfico predeterminada puesto como \"sin unidad\" por la edición a este grupo", - "group.edit.verify": "Dados estos mensajes, ¿quieres cancelar este cambio (selecciona Cancelar) o continuar (selecciona OK)?", + "group.edit.verify": "Dados estos mensajes, ¿quieres continuar con este cambio?", "group.failed.to.create.group": "No se pudo crear un grupo con el mensaje: ", "group.failed.to.edit.group": "No se pudo editar el grupo con el mensaje: ", "group.gps.error": "Por favor indique un punto GPS valído: (latitud, longitud)", @@ -1582,7 +1591,7 @@ const LocaleTranslationData = { "map.calibrate": "Calibrar", "map.calibration": "Estado de calibración", "map.circle.size": "Tamaño del círculo en el mapa", - "map.confirm.remove": "¿Estás seguro de que quieres eliminar el mapa", + "map.confirm.remove": "¿Estás seguro de que quieres eliminar el mapa «{name}»?", "map.displayable": "Visuilización del mapa", "map.filename": "Archivo del mapa", "map.id": "ID del Mapa", @@ -1613,7 +1622,7 @@ const LocaleTranslationData = { "meter.cumulativeResetEnd": "Final del reinicio cumulativo:", "meter.cumulativeResetStart": "Comienzo del reinicio cumulativo:", "meter.disableChecks": "Desactivar revisiones", - "meter.edit.displayable.verify": "Given the group(s) listed above, do you want to cancel this change (click Cancel) or continue (click OK)?\u{26A1}", + "meter.edit.displayable.verify": "Dados los grupos indicados arriba, ¿quieres continuar con este cambio?", "meter.edit.displayable.warning": "is not displayable but is used by the following displayable groups:\u{26A1}", "meter.enabled": "Medidor activado", "meter.endOnlyTime": "Solo tiempos finales.",