Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion src/client/app/components/ExportComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,53 @@
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
*/
export default function ExportComponent() {
const dispatch = useAppDispatch();
const chartToRender = useAppSelector(selectChartToRender);
const translate = useTranslate();
const [pendingExportConfirmation, setPendingExportConfirmation] = React.useState<PendingExportConfirmation | null>(null);
const pendingExportConfirmationRef = React.useRef<PendingExportConfirmation | null>(null);

const requestExportConfirmation = React.useCallback((message: string) => new Promise<boolean>(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 (
<>
Expand All @@ -33,11 +68,21 @@ export default function ExportComponent() {
/* Only raw export if a line graph */
chartToRender === ChartTypes.line &&
<div style={{ paddingTop: '10px' }}>
<Button color='secondary' outline onClick={() => dispatch(exportRawReadings())}>
<Button color='secondary' outline
onClick={() => dispatch(exportRawReadings({ requestConfirmation: requestExportConfirmation }))}>
<FormattedMessage id='export.raw.graph.data' />
</Button>
</div>
}
<ConfirmActionModalComponent
show={pendingExportConfirmation !== null}
actionTitle={translate('confirm.action')}
actionConfirmMessage={pendingExportConfirmation?.message}
handleClose={() => settleExportConfirmation(false)}
actionFunction={() => settleExportConfirmation(true)}
actionRejectText={translate('cancel')}
actionConfirmText={translate('continue')}
/>
</>
);
}
36 changes: 31 additions & 5 deletions src/client/app/components/groups/CreateGroupModalComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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'));
}
Expand All @@ -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();
Expand Down Expand Up @@ -370,6 +387,15 @@ export default function CreateGroupModalComponent() {
disabled={!canSave}
/>
)}
<ConfirmActionModalComponent
show={pendingAreaCalculation !== null}
actionTitle={translate('group.area.calculate')}
actionConfirmMessage={pendingAreaCalculation?.message}
handleClose={handleAreaCalculationCancel}
actionFunction={handleAreaCalculationConfirm}
actionRejectText={translate('cancel')}
actionConfirmText={translate('group.area.calculate.update')}
/>
{/* Show modal button */}
<Button color='secondary' onClick={handleShow}>
<FormattedMessage id="create.group" />
Expand Down
83 changes: 70 additions & 13 deletions src/client/app/components/groups/EditGroupModalComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr
};

/* State */
const [pendingAreaCalculation, setPendingAreaCalculation] = useState<{ message: string; area: number } | null>(null);
const [pendingChildAssignment, setPendingChildAssignment] = useState<{
message: string;
resolve: (shouldUpdate: boolean) => void;
} | null>(null);
// Handlers for each type of input change where update the local edit state.

const handleStringChange = (e: React.ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -233,20 +238,18 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr
notifyMsg += '\n"' + meter.identifier + '"' + translate('group.area.calculate.error.zero');
}
});
let msg = translate('group.area.calculate.header') + areaSum + ' ' + translate(`AreaUnitType.${groupState.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.${groupState.areaUnit}`) + '?';
if (notifyMsg != '') {
msg += '\n' + translate('group.area.calculate.error.header') + notifyMsg;
}
if (window.confirm(msg)) {
setEditGroupsState({
...editGroupsState,
[groupState.id]: {
...editGroupsState[groupState.id],
// the + here converts back into a number. this method also removes trailing zeroes.
['area']: +areaSum.toPrecision(6)
}
});
}
setPendingAreaCalculation({
message: msg,
area: roundedArea
});
} else {
showErrorNotification(translate('group.area.calculate.error.group.unit'));
}
Expand All @@ -255,6 +258,33 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr
}
};

const handleAreaCalculationConfirm = () => {
if (pendingAreaCalculation !== null) {
setEditGroupsState(currentState => ({
...currentState,
[groupState.id]: {
...currentState[groupState.id],
area: pendingAreaCalculation.area
}
}));
}
setPendingAreaCalculation(null);
};

const handleAreaCalculationCancel = () => {
setPendingAreaCalculation(null);
};

const handleChildAssignmentConfirm = () => {
pendingChildAssignment?.resolve(true);
setPendingChildAssignment(null);
};

const handleChildAssignmentCancel = () => {
pendingChildAssignment?.resolve(false);
setPendingChildAssignment(null);
};

// Reset the state to default values.
// To be used for the discard changes button
// Different use case from CreateGroupModalComponent's resetState
Expand Down Expand Up @@ -501,6 +531,24 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr
disabled={!canSave || !validGroup}
/>
)}
<ConfirmActionModalComponent
show={pendingAreaCalculation !== null}
actionTitle={translate('group.area.calculate')}
actionConfirmMessage={pendingAreaCalculation?.message}
handleClose={handleAreaCalculationCancel}
actionFunction={handleAreaCalculationConfirm}
actionRejectText={translate('cancel')}
actionConfirmText={translate('group.area.calculate.update')}
/>
<ConfirmActionModalComponent
show={pendingChildAssignment !== null}
actionTitle={translate('confirm.action')}
actionConfirmMessage={pendingChildAssignment?.message}
handleClose={handleChildAssignmentCancel}
actionFunction={handleChildAssignmentConfirm}
actionRejectText={translate('cancel')}
actionConfirmText={translate('continue')}
/>
{/* This is for the modal for delete. */}
<ConfirmActionModalComponent
show={showDeleteConfirmationModal}
Expand Down Expand Up @@ -872,7 +920,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr
* @param groupsState The local group state to use.
* @returns true if change fine or if admin agreed. false if admin does not or the change is an issue.
*/
function validateGroupPostAddChild(gid: number, parentGroupIds: number[], groupsState: any): boolean {
async function validateGroupPostAddChild(gid: number, parentGroupIds: number[], groupsState: any): Promise<boolean> {
// This will hold the overall message for the admin alert.
let msg = '';
// Tells if the change should be cancelled.
Expand Down Expand Up @@ -917,7 +965,16 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr
} else {
// If msg is not empty, warns the admin and asks if they want to apply changes.
msg += `\n${translate('group.edit.verify')}`;
cancel = !window.confirm(msg);
return new Promise<boolean>(resolve => {
let resolved = false;
const resolveOnce = (shouldUpdate: boolean) => {
if (!resolved) {
resolved = true;
resolve(shouldUpdate);
}
};
setPendingChildAssignment({ message: msg, resolve: resolveOnce });
});
}
}
return !cancel;
Expand Down
40 changes: 34 additions & 6 deletions src/client/app/components/maps/MapViewComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Button } from 'reactstrap';
import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map';
import { showErrorNotification } from '../../utils/notifications';
import { hasToken } from '../../utils/token';
import ConfirmActionModalComponent from '../ConfirmActionModalComponent';

interface MapViewProps {
// The ID of the map to be displayed
Expand All @@ -32,6 +33,7 @@ interface MapViewState {
circleInput: string;
noteFocus: boolean;
noteInput: string;
showDeleteConfirmationModal: boolean;
}

type MapViewPropsWithIntl = MapViewProps & WrappedComponentProps;
Expand All @@ -46,7 +48,8 @@ class MapViewComponent extends React.Component<MapViewPropsWithIntl, MapViewStat
noteInput: (this.props.map.note) ? this.props.map.note : '',
circleFocus: false,
// circleSize should always be a valid string due to how stored and mapRow.
circleInput: this.props.map.circleSize.toString()
circleInput: this.props.map.circleSize.toString(),
showDeleteConfirmationModal: false
};
this.handleCalibrationSetting = this.handleCalibrationSetting.bind(this);
this.toggleMapDisplayable = this.toggleMapDisplayable.bind(this);
Expand All @@ -55,6 +58,8 @@ class MapViewComponent extends React.Component<MapViewPropsWithIntl, MapViewStat
this.toggleNoteInput = this.toggleNoteInput.bind(this);
this.handleNoteChange = this.handleNoteChange.bind(this);
this.toggleDelete = this.toggleDelete.bind(this);
this.handleDeleteConfirmationClose = this.handleDeleteConfirmationClose.bind(this);
this.handleDeleteMap = this.handleDeleteMap.bind(this);
this.notifyCalibrationNeeded = this.notifyCalibrationNeeded.bind(this);
this.handleSizeChange = this.handleSizeChange.bind(this);
this.toggleCircleInput = this.toggleCircleInput.bind(this);
Expand Down Expand Up @@ -197,18 +202,41 @@ class MapViewComponent extends React.Component<MapViewPropsWithIntl, MapViewStat
}

private toggleDelete() {
const consent = window.confirm(`${this.props.intl.formatMessage({ id: 'map.confirm.remove' })} "${this.props.map.name}"?`);
if (consent) { this.props.removeMap(this.props.id); }
this.setState({ showDeleteConfirmationModal: true });
}

private handleDeleteConfirmationClose() {
this.setState({ showDeleteConfirmationModal: false });
}

private handleDeleteMap() {
this.setState({ showDeleteConfirmationModal: false });
this.props.removeMap(this.props.id);
}

private formatDeleteButton() {
const editButtonStyle: React.CSSProperties = {
display: 'inline', // or 'none'
paddingLeft: '5px'
};
return <Button style={editButtonStyle} color='primary' onClick={this.toggleDelete}>
<FormattedMessage id={'delete.map'} />
</Button>;
const deleteConfirmationMessage = this.props.intl.formatMessage(
{ id: 'map.confirm.remove' },
{ name: this.props.map.name }
);
return <>
<Button style={editButtonStyle} color='primary' onClick={this.toggleDelete}>
<FormattedMessage id={'delete.map'} />
</Button>
<ConfirmActionModalComponent
show={this.state.showDeleteConfirmationModal}
actionTitle={this.props.intl.formatMessage({ id: 'delete.map' })}
actionConfirmMessage={deleteConfirmationMessage}
handleClose={this.handleDeleteConfirmationClose}
actionFunction={this.handleDeleteMap}
actionRejectText={this.props.intl.formatMessage({ id: 'cancel' })}
actionConfirmText={this.props.intl.formatMessage({ id: 'delete.map' })}
/>
</>;
}

private styleEnabled(): React.CSSProperties {
Expand Down
Loading