From 6f3ee9fa3ebaa693e2a3a0f005c759242449bbea Mon Sep 17 00:00:00 2001 From: Juan Gutierrez <juanjoseguva@gmail.com> Date: Thu, 18 Jul 2024 11:02:17 -0700 Subject: [PATCH 01/50] Changing maps page to cards with modals --- .../maps/CreateMapModalComponent.tsx | 72 +++ .../components/maps/EditMapModalComponent.tsx | 147 ++++++ .../app/components/maps/MapViewComponent.tsx | 481 +++--------------- .../components/maps/MapsDetailComponent.tsx | 100 ++-- 4 files changed, 321 insertions(+), 479 deletions(-) create mode 100644 src/client/app/components/maps/CreateMapModalComponent.tsx create mode 100644 src/client/app/components/maps/EditMapModalComponent.tsx diff --git a/src/client/app/components/maps/CreateMapModalComponent.tsx b/src/client/app/components/maps/CreateMapModalComponent.tsx new file mode 100644 index 000000000..bde6ab39e --- /dev/null +++ b/src/client/app/components/maps/CreateMapModalComponent.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input } from 'reactstrap'; +import { Link } from 'react-router-dom'; +import { CalibrationModeTypes } from '../../types/redux/map'; + +interface CreateMapModalProps { + show: boolean; + handleClose: () => void; + createNewMap: () => void; +} + +/** + * + */ +function CreateMapModalComponent({ show, handleClose, createNewMap }: CreateMapModalProps) { + const [nameInput, setNameInput] = useState(''); + const [noteInput, setNoteInput] = useState(''); + + const handleCreate = () => { + // TODO: Implement create functionality + createNewMap(); + handleClose(); + }; + + return ( + <Modal isOpen={show} toggle={handleClose}> + <ModalHeader toggle={handleClose}> + <FormattedMessage id="create.map" /> + </ModalHeader> + <ModalBody> + <Form> + <FormGroup> + <Label for="mapName"><FormattedMessage id="map.name" /></Label> + <Input + id="mapName" + value={nameInput} + onChange={(e) => setNameInput(e.target.value)} + /> + </FormGroup> + <FormGroup> + <Label for="mapNote"><FormattedMessage id="note" /></Label> + <Input + id="mapNote" + type="textarea" + value={noteInput} + onChange={(e) => setNoteInput(e.target.value)} + /> + </FormGroup> + </Form> + <div> + <Link to='/calibration' onClick={() => createNewMap()}> + <Button color='primary'> + <FormattedMessage id='map.upload.file' /> + </Button> + </Link> + </div> + </ModalBody> + <ModalFooter> + <Button color="secondary" onClick={handleClose}> + <FormattedMessage id="cancel" /> + </Button> + <Button color="primary" onClick={handleCreate}> + <FormattedMessage id="create" /> + </Button> + </ModalFooter> + </Modal> + ); +} + +export default CreateMapModalComponent; \ No newline at end of file diff --git a/src/client/app/components/maps/EditMapModalComponent.tsx b/src/client/app/components/maps/EditMapModalComponent.tsx new file mode 100644 index 000000000..b1054ccca --- /dev/null +++ b/src/client/app/components/maps/EditMapModalComponent.tsx @@ -0,0 +1,147 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input } from 'reactstrap'; +import { Link } from 'react-router-dom'; +import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; +import { showErrorNotification } from '../../utils/notifications'; + +interface EditMapModalProps { + show: boolean; + handleClose: () => void; + map: MapMetadata; + editMapDetails(map: MapMetadata): any; + setCalibration(mode: CalibrationModeTypes, mapID: number): any; + removeMap(id: number): any; +} + +/** + *Defines the edit maps modal form + * @param props state variables needed to define the component + * @returns Map edit element + */ +function EditMapModalComponent(props: EditMapModalProps) { + const [nameInput, setNameInput] = useState(props.map.name); + const [noteInput, setNoteInput] = useState(props.map.note || ''); + const [circleInput, setCircleInput] = useState(props.map.circleSize.toString()); + const [displayable, setDisplayable] = useState(props.map.displayable); + + const intl = useIntl(); + + const handleSave = () => { + const updatedMap = { + ...props.map, + name: nameInput, + note: noteInput, + circleSize: parseFloat(circleInput), + displayable: displayable + }; + props.editMapDetails(updatedMap); + props.handleClose(); + }; + + const handleDelete = () => { + const consent = window.confirm(intl.formatMessage({ id: 'map.confirm.remove' }, { name: props.map.name })); + if (consent) { + props.removeMap(props.map.id); + props.handleClose(); + } + }; + + const handleCalibrationSetting = (mode: CalibrationModeTypes) => { + props.setCalibration(mode, props.map.id); + props.handleClose(); + }; + + const toggleCircleEdit = () => { + const regtest = /^\d+(\.\d+)?$/; + if (regtest.test(circleInput) && parseFloat(circleInput) <= 2.0) { + setCircleInput(circleInput); + } else { + showErrorNotification(intl.formatMessage({ id: 'invalid.number' })); + } + }; + + return ( + <Modal isOpen={props.show} toggle={props.handleClose}> + <ModalHeader toggle={props.handleClose}> + <FormattedMessage id="edit.map" /> + </ModalHeader> + <ModalBody> + <Form> + <FormGroup> + <Label for="mapName"><FormattedMessage id="map.name" /></Label> + <Input + id="mapName" + value={nameInput} + onChange={e => setNameInput(e.target.value)} + /> + </FormGroup> + <FormGroup> + <Label for="mapDisplayable"><FormattedMessage id="map.displayable" /></Label> + <Input + id="mapDisplayable" + type="select" + value={displayable.toString()} + onChange={e => setDisplayable(e.target.value === 'true')} + > + <option value="true">{intl.formatMessage({ id: 'map.is.displayable' })}</option> + <option value="false">{intl.formatMessage({ id: 'map.is.not.displayable' })}</option> + </Input> + </FormGroup> + <FormGroup> + <Label for="mapCircleSize"><FormattedMessage id="map.circle.size" /></Label> + <Input + id="mapCircleSize" + value={circleInput} + onChange={e => setCircleInput(e.target.value)} + onBlur={toggleCircleEdit} + /> + </FormGroup> + <FormGroup> + <Label for="mapNote"><FormattedMessage id="note" /></Label> + <Input + id="mapNote" + type="textarea" + value={noteInput} + onChange={e => setNoteInput(e.target.value)} + /> + </FormGroup> + </Form> + <div> + <Label><FormattedMessage id="map.filename" /></Label> + <p>{props.map.filename}</p> + <Link to='/calibration' onClick={() => handleCalibrationSetting(CalibrationModeTypes.initiate)}> + <Button color='primary'> + <FormattedMessage id='map.upload.new.file' /> + </Button> + </Link> + </div> + <div> + <Label><FormattedMessage id="map.calibration" /></Label> + <p> + <FormattedMessage id={props.map.origin && props.map.opposite ? 'map.is.calibrated' : 'map.is.not.calibrated'} /> + </p> + <Link to='/calibration' onClick={() => handleCalibrationSetting(CalibrationModeTypes.calibrate)}> + <Button color='primary'> + <FormattedMessage id='map.calibrate' /> + </Button> + </Link> + </div> + </ModalBody> + <ModalFooter> + <Button color="danger" onClick={handleDelete}> + <FormattedMessage id="delete.map" /> + </Button> + <Button color="secondary" onClick={props.handleClose}> + <FormattedMessage id="cancel" /> + </Button> + <Button color="primary" onClick={handleSave}> + <FormattedMessage id="done.editing" /> + </Button> + </ModalFooter> + </Modal> + ); +} + +export default EditMapModalComponent; \ No newline at end of file diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index b8b91a698..28b853638 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -2,436 +2,91 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as moment from 'moment'; import * as React from 'react'; -import { FormattedMessage, injectIntl, WrappedComponentProps } from 'react-intl'; -import { Link } from 'react-router-dom'; +import { useState, useEffect } from 'react'; +import { FormattedMessage} from 'react-intl'; import { Button } from 'reactstrap'; +import * as moment from 'moment'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; -import { showErrorNotification } from '../../utils/notifications'; import { hasToken } from '../../utils/token'; +import '../../styles/card-page.css'; +import EditMapModalComponent from './EditMapModalComponent'; + interface MapViewProps { - // The ID of the map to be displayed id: number; - // The map metadata being displayed by this row map: MapMetadata; isEdited: boolean; isSubmitting: boolean; - // The function used to dispatch the action to edit map details editMapDetails(map: MapMetadata): any; setCalibration(mode: CalibrationModeTypes, mapID: number): any; removeMap(id: number): any; } -interface MapViewState { - nameFocus: boolean; - nameInput: string; - circleFocus: boolean; - circleInput: string; - noteFocus: boolean; - noteInput: string; -} - -type MapViewPropsWithIntl = MapViewProps & WrappedComponentProps; - -class MapViewComponent extends React.Component<MapViewPropsWithIntl, MapViewState> { - constructor(props: MapViewPropsWithIntl) { - super(props); - this.state = { - nameFocus: false, - nameInput: this.props.map.name, - noteFocus: false, - 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() - }; - this.handleCalibrationSetting = this.handleCalibrationSetting.bind(this); - this.toggleMapDisplayable = this.toggleMapDisplayable.bind(this); - this.toggleNameInput = this.toggleNameInput.bind(this); - this.handleNameChange = this.handleNameChange.bind(this); - this.toggleNoteInput = this.toggleNoteInput.bind(this); - this.handleNoteChange = this.handleNoteChange.bind(this); - this.toggleDelete = this.toggleDelete.bind(this); - this.notifyCalibrationNeeded = this.notifyCalibrationNeeded.bind(this); - this.handleSizeChange = this.handleSizeChange.bind(this); - this.toggleCircleInput = this.toggleCircleInput.bind(this); - } - - public render() { - return ( - <tr> - <td> {this.props.map.id} {this.formatStatus()}</td> - <td> {this.formatName()} </td> - {hasToken() && <td> {this.formatDisplayable()} </td>} - {hasToken() && <td> {this.formatCircleSize()} </td>} - {/* This was stored as UTC but with the local time at that point. - Thus, moment will not modify the date/time given when done this way. */} - {hasToken() && <td> {moment.parseZone(this.props.map.modifiedDate, undefined, true).format('dddd, MMM DD, YYYY hh:mm a')} </td>} - {hasToken() && <td> {this.formatFilename()} </td>} - {hasToken() && <td> {this.formatNote()} </td>} - {hasToken() && <td> {this.formatCalibrationStatus()} </td>} - {hasToken() && <td> {this.formatDeleteButton()} </td>} - </tr> - ); - } - - componentDidMount() { - if (this.props.isEdited) { - // When the props.isEdited is true after loading the page, there are unsaved changes - this.updateUnsavedChanges(); - } - } - - componentDidUpdate(prevProps: MapViewProps) { - if (this.props.isEdited && !prevProps.isEdited) { - // When the props.isEdited changes from false to true, there are unsaved changes - this.updateUnsavedChanges(); - } - } - - // Re-implement After RTK migration - // private removeUnsavedChangesFunction(callback: () => void) { - // // This function is called to reset all the inputs to the initial state - // store.dispatch<any>(confirmEditedMaps()).then(() => { - // store.dispatch<any>(fetchMapsDetails()).then(callback); - // }); - // } - - // Re-implement After RTK migration - // private submitUnsavedChangesFunction(successCallback: () => void, failureCallback: () => void) { - // // This function is called to submit the unsaved changes - // store.dispatch<any>(submitEditedMaps()).then(successCallback, failureCallback); - // } - - private updateUnsavedChanges() { - // Re-implement After RTK migration - // Notify that there are unsaved changes - // store.dispatch(unsavedWarningSlice.actions.updateUnsavedChanges({ - // removeFunction: this.removeUnsavedChangesFunction, - // submitFunction: this.submitUnsavedChangesFunction - // })); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } - - private handleSizeChange(event: React.ChangeEvent<HTMLTextAreaElement>) { - this.setState({ circleInput: event.target.value }); - } - - private toggleCircleInput() { - let checkval: boolean = true; - // if trying to submit an updated value - if (this.state.circleFocus) { - const regtest = /^\d+(\.\d+)?$/; - checkval = regtest.test(this.state.circleInput); - if (checkval) { - if (parseFloat(this.state.circleInput) > 2.0) { - checkval = false; - } - else { - const editedMap = { - ...this.props.map, - circleSize: parseFloat(this.state.circleInput) - }; - this.props.editMapDetails(editedMap); - } - } - } - if (checkval) { - this.setState({ circleFocus: !this.state.circleFocus }); - } - else { - showErrorNotification(`${this.props.intl.formatMessage({ id: 'invalid.number' })}`); - } - } - - private formatCircleSize() { - let formattedCircleSize; - let buttonMessageId; - if (this.state.circleFocus) { - // default value for autoFocus is true and for all attributes that would be set autoFocus={true} - formattedCircleSize = <textarea id={'csize'} autoFocus value={this.state.circleInput} onChange={event => this.handleSizeChange(event)} />; - buttonMessageId = 'update'; - } else { - formattedCircleSize = <div>{this.state.circleInput}</div>; - buttonMessageId = 'edit'; - } - - let toggleButton; - if (hasToken()) { - toggleButton = <Button style={this.styleToggleBtn()} color='primary' onClick={this.toggleCircleInput}> - <FormattedMessage id={buttonMessageId} /> - </Button>; - } else { - toggleButton = <div />; - } - - if (hasToken()) { - return ( - <div> - {formattedCircleSize} - {toggleButton} - </div> - ); - } else { - return ( - <div> - {this.props.map.circleSize} - {toggleButton} - </div> - ); - } - } - - private formatStatus(): string { - if (this.props.isSubmitting) { - return '(' + this.props.intl.formatMessage({ id: 'submitting' }) + ')'; - } - if (this.props.isEdited) { - return this.props.intl.formatMessage({ id: 'edited' }); - } - return ''; - } - - 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); } - } - - 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>; - } - - private styleEnabled(): React.CSSProperties { - return { color: 'green' }; - } - - private styleDisabled(): React.CSSProperties { - return { color: 'red' }; - } - - private styleToggleBtn(): React.CSSProperties { - return { float: 'right' }; - } - - private toggleMapDisplayable() { - const editedMap = { - ...this.props.map, - displayable: !this.props.map.displayable - }; - this.props.editMapDetails(editedMap); - } - - private formatDisplayable() { - let styleFn; - let messageId; - let buttonMessageId; - - if (this.props.map.displayable) { - styleFn = this.styleEnabled; - messageId = 'map.is.displayable'; - buttonMessageId = 'hide'; - } else { - styleFn = this.styleDisabled; - messageId = 'map.is.not.displayable'; - buttonMessageId = 'show'; - } - - let toggleButton; - if (hasToken()) { - // throw out alert if the admin wants to display uncalibrated map - if (!(this.props.map.origin && this.props.map.opposite)) { - toggleButton = <Button style={this.styleToggleBtn()} color='primary' onClick={this.notifyCalibrationNeeded}> - <FormattedMessage id={buttonMessageId} /> - </Button>; - } - // if map is already calibrated, the button will allow it to be displayed - else { - toggleButton = <Button style={this.styleToggleBtn()} color='primary' onClick={this.toggleMapDisplayable}> - <FormattedMessage id={buttonMessageId} /> - </Button>; - } - } else { - toggleButton = <div />; - } - - return ( - <span> - <span style={styleFn()}> - <FormattedMessage id={messageId} /> +/** + * Defines the map info card + * @param props variables passed in to define + * @returns Map info card element + */ +function MapViewComponent(props: MapViewProps) { + const [showEditModal, setShowEditModal] = useState(false); + + useEffect(() => { + if (props.isEdited) { + //updateUnsavedChanges(); + } + }, [props.isEdited]); + + const handleShowModal = () => setShowEditModal(true); + const handleCloseModal = () => setShowEditModal(false); + + return ( + <div className="card"> + <div className="identifier-container"> + {props.map.name} {props.isSubmitting ? '(Submitting)' : props.isEdited ? '(Edited)' : ''} + </div> + <div className="item-container"> + <b><FormattedMessage id="map.displayable" /></b> + <span style={{ color: props.map.displayable ? 'green' : 'red' }}> + <FormattedMessage id={props.map.displayable ? 'map.is.displayable' : 'map.is.not.displayable'} /> </span> - {toggleButton} - </span> - ); - } - - // this function throws alert on the browser notifying that map needs calibrating before display - private notifyCalibrationNeeded() { - showErrorNotification(`${this.props.intl.formatMessage({ id: 'map.notify.calibration.needed' })} "${this.props.map.name}"`); - } - - private toggleNameInput() { - if (this.state.nameFocus) { - const editedMap = { - ...this.props.map, - name: this.state.nameInput - }; - this.props.editMapDetails(editedMap); - } - this.setState({ nameFocus: !this.state.nameFocus }); - } - - private handleNameChange(event: React.ChangeEvent<HTMLTextAreaElement>) { - this.setState({ nameInput: event.target.value }); - } - - private formatName() { - let formattedName; - let buttonMessageId; - if (this.state.nameFocus) { - // default value for autoFocus is true and for all attributes that would be set autoFocus={true} - formattedName = <textarea id={'name'} autoFocus value={this.state.nameInput} onChange={event => this.handleNameChange(event)} />; - buttonMessageId = 'update'; - } else { - formattedName = <div>{this.state.nameInput}</div>; - buttonMessageId = 'edit'; - } - - let toggleButton; - if (hasToken()) { - toggleButton = <Button style={this.styleToggleBtn()} color='primary' onClick={this.toggleNameInput}> - <FormattedMessage id={buttonMessageId} /> - </Button>; - } else { - toggleButton = <div />; - } - - if (hasToken()) { - return ( - <div> - {formattedName} - {toggleButton} - </div> - ); - } else { - return ( - <div> - {this.props.map.name} - {toggleButton} - </div> - ); - } - } - - private toggleNoteInput() { - if (this.state.noteFocus) { - const editedMap = { - ...this.props.map, - note: this.state.noteInput - }; - this.props.editMapDetails(editedMap); - } - this.setState({ noteFocus: !this.state.noteFocus }); - } - - private handleNoteChange(event: React.ChangeEvent<HTMLTextAreaElement>) { - this.setState({ noteInput: event.target.value }); - } - - private formatNote() { - let formattedNote; - let buttonMessageId; - if (this.state.noteFocus) { - // default value for autoFocus is true and for all attributes that would be set autoFocus={true} - formattedNote = <textarea id={'note'} autoFocus value={this.state.noteInput} onChange={event => this.handleNoteChange(event)} />; - buttonMessageId = 'update'; - } else { - formattedNote = <div>{this.state.noteInput}</div>; - buttonMessageId = 'edit'; - } - - let toggleButton; - if (hasToken()) { - toggleButton = <Button style={this.styleToggleBtn()} color='primary' onClick={this.toggleNoteInput}> - <FormattedMessage id={buttonMessageId} /> - </Button>; - } else { - toggleButton = <div />; - } - - if (hasToken()) { - return ( - <div> - {formattedNote} - {toggleButton} - </div> - ); - } else { - return ( - <div> - {this.props.map.note} - {toggleButton} - </div> - ); - } - } - - private styleCalibrated(): React.CSSProperties { - return { color: 'black' }; - } - - private styleNotCalibrated(): React.CSSProperties { - return { color: 'gray' }; - } - - private formatCalibrationStatus() { - let styleFn; - let messageID; - if (this.props.map.origin && this.props.map.opposite) { - styleFn = this.styleCalibrated; - messageID = 'map.is.calibrated'; - } else { - styleFn = this.styleNotCalibrated; - messageID = 'map.is.not.calibrated'; - } - return ( - <span> - <span style={styleFn()}> - <FormattedMessage id={messageID} /> + </div> + <div className="item-container"> + <b><FormattedMessage id="map.circle.size" /></b> {props.map.circleSize} + </div> + <div className="item-container"> + <b><FormattedMessage id="map.modified.date" /></b> + {moment.parseZone(props.map.modifiedDate, undefined, true).format('dddd, MMM DD, YYYY hh:mm a')} + </div> + <div className="item-container"> + <b><FormattedMessage id="map.filename" /></b> {props.map.filename} + </div> + <div className="item-container"> + <b><FormattedMessage id="note" /></b> {props.map.note} + </div> + <div className="item-container"> + <b><FormattedMessage id="map.calibration" /></b> + <span style={{ color: props.map.origin && props.map.opposite ? 'black' : 'gray' }}> + <FormattedMessage id={props.map.origin && props.map.opposite ? 'map.is.calibrated' : 'map.is.not.calibrated'} /> </span> - <Link to='/calibration' onClick={() => this.handleCalibrationSetting(CalibrationModeTypes.calibrate)}> - <Button style={this.styleToggleBtn()} color='primary'> - <FormattedMessage id='map.calibrate' /> + </div> + {hasToken() && ( + <div className="edit-btn"> + <Button color='secondary' onClick={handleShowModal}> + <FormattedMessage id="edit.map" /> </Button> - </Link> - </span> - ); - } - - private formatFilename() { - return ( - <span> - <span>{this.props.map.filename}</span> - <Link to='/calibration' onClick={() => this.handleCalibrationSetting(CalibrationModeTypes.initiate)}> - <Button style={this.styleToggleBtn()} color='primary'> - <FormattedMessage id='map.upload.new.file' /> - </Button> - </Link> - </span> - ); - } - - private handleCalibrationSetting(mode: CalibrationModeTypes) { - this.props.setCalibration(mode, this.props.id); - this.updateUnsavedChanges(); - } + </div> + )} + <EditMapModalComponent + show={showEditModal} + handleClose={handleCloseModal} + map={props.map} + editMapDetails={props.editMapDetails} + setCalibration={props.setCalibration} + removeMap={props.removeMap} + /> + </div> + ); } -export default injectIntl(MapViewComponent); +export default MapViewComponent; \ No newline at end of file diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index 2cc9868d0..d154133da 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -5,11 +5,12 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; -import { Button, Table } from 'reactstrap'; +import { Button } from 'reactstrap'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import MapViewContainer from '../../containers/maps/MapViewContainer'; import { hasToken } from '../../utils/token'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import '../../styles/card-page.css'; interface MapsDetailProps { maps: number[]; @@ -30,31 +31,8 @@ export default class MapsDetailComponent extends React.Component<MapsDetailProps } public render() { - - const titleStyle: React.CSSProperties = { - textAlign: 'center' - }; - - const tableStyle: React.CSSProperties = { - marginLeft: '5%', - marginRight: '5%' - }; - - const buttonContainerStyle: React.CSSProperties = { - minWidth: '150px', - width: '10%', - marginLeft: '40%', - marginRight: '40%' - }; - - const tooltipStyle = { - display: 'inline-block', - fontSize: '50%' - }; - return ( <div className='flexGrowOne'> - {/* <UnsavedWarningContainer /> */} <TooltipHelpComponent page='maps' /> <div className='container-fluid'> <h2 style={titleStyle}> @@ -63,56 +41,46 @@ export default class MapsDetailComponent extends React.Component<MapsDetailProps <TooltipMarkerComponent page='maps' helpTextId='help.admin.mapview' /> </div> </h2> - <div style={tableStyle}> - <Table striped bordered hover> - <thead> - <tr> - <th> <FormattedMessage id='map.id' /> </th> - <th> <FormattedMessage id='map.name' /> </th> - {hasToken() && <th> <FormattedMessage id='map.displayable' /> </th>} - {hasToken() && <th> <FormattedMessage id='map.circle.size' /> </th>} - {hasToken() && <th> <FormattedMessage id='map.modified.date' /> </th>} - {hasToken() && <th> <FormattedMessage id='map.filename' /> </th>} - {hasToken() && <th> <FormattedMessage id='note' /> </th>} - {hasToken() && <th> <FormattedMessage id='map.calibration' /> </th>} - {hasToken() && <th> <FormattedMessage id='remove' /> </th>} - </tr> - </thead> - <tbody> - {this.props.maps.map(mapID => - (<MapViewContainer key={mapID} id={mapID} />))} - <tr> - <td colSpan={8}> - <Link to='/calibration' onClick={() => this.props.createNewMap()}> - <Button style={buttonContainerStyle} color='primary'> - <FormattedMessage id='create.map' /> - </Button> - </Link> - </td> - </tr> - </tbody> - </Table> + <div className="edit-btn"> + <Link to='/calibration' onClick={() => this.props.createNewMap()}> + <Button color='primary'> + <FormattedMessage id='create.map' /> + </Button> + </Link> + </div> + <div className="card-container"> + {this.props.maps.map(mapID => ( + <MapViewContainer key={mapID} id={mapID} /> + ))} </div> - {hasToken() && <Button - color='success' - style={buttonContainerStyle} - disabled={!this.props.unsavedChanges} - onClick={this.handleSubmitClicked} - > - <FormattedMessage id='save.map.edits' /> - </Button>} + {hasToken() && ( + <div className="edit-btn"> + <Button + color='success' + disabled={!this.props.unsavedChanges} + onClick={this.handleSubmitClicked} + > + <FormattedMessage id='save.map.edits' /> + </Button> + </div> + )} </div> </div> ); } - private removeUnsavedChanges() { - // store.dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); - } - private handleSubmitClicked() { this.props.submitEditedMaps(); // Notify that the unsaved changes have been submitted - this.removeUnsavedChanges(); + // this.removeUnsavedChanges(); } } + +const titleStyle: React.CSSProperties = { + textAlign: 'center' +}; + +const tooltipStyle = { + display: 'inline-block', + fontSize: '50%' +}; \ No newline at end of file From 17d26a069184eceb9214c8a5dbe796785ae40499 Mon Sep 17 00:00:00 2001 From: Juan Gutierrez <juanjoseguva@gmail.com> Date: Thu, 18 Jul 2024 11:16:53 -0700 Subject: [PATCH 02/50] Adding messagesfor map modal --- src/client/app/translations/data.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index aa9ff683a..213651a02 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -151,6 +151,7 @@ const LocaleTranslationData = { "DisplayableType.none": "none", "DisplayableType.all": "all", "DisplayableType.admin": "admin", + "done.editing":"Done editing", "error.bounds": "Must be between {min} and {max}.", "error.displayable": "Displayable will be set to false because no unit is selected.", "error.displayable.meter": "Meter units will set displayable to none.", @@ -165,6 +166,7 @@ const LocaleTranslationData = { "edit.a.group": "Edit a Group", "edit.a.meter": "Edit a Meter", "edit.group": "Edit Group", + "edit.map":"Edit Map", "edit.meter": "Details/Edit Meter", "edit.unit": "Edit Unit", "email": "Email", @@ -646,6 +648,7 @@ const LocaleTranslationData = { "DisplayableType.none": "none\u{26A1}", "DisplayableType.all": "all\u{26A1}", "DisplayableType.admin": "admin\u{26A1}", + "done.editing":"Done editing\u{26A1}", "error.bounds": "Must be between {min} and {max}.\u{26A1}", "error.displayable": "Displayable will be set to false because no unit is selected.\u{26A1}", "error.displayable.meter": "Meter units will set displayable to none.\u{26A1}", @@ -660,6 +663,7 @@ const LocaleTranslationData = { "edit.a.group": "Modifier le Groupe", "edit.a.meter": "Modifier le Métre", "edit.group": "Modifier Groupe", + "edit.map":"Edit Map\u{26A1}", "edit.meter": "Details/Modifier Métre\u{26A1}", "edit.unit": "Edit Unit\u{26A1}", "email": "E-mail", @@ -1141,6 +1145,7 @@ const LocaleTranslationData = { "DisplayableType.none": "ninguno", "DisplayableType.all": "todo", "DisplayableType.admin": "administrador", + "done.editing":"Acabar de editar", "error.bounds": "Debe ser entre {min} y {max}.", "error.displayable": "El elemento visual determinado como falso porque no hay unidad seleccionada.", "error.displayable.meter": "Las unidades de medición determinarán al elemento visual como ninguno.", @@ -1155,7 +1160,8 @@ const LocaleTranslationData = { "edit.a.group": "Editar un grupo", "edit.a.meter": "Editar un medidor", "edit.group": "Editar grupo", - "edit.meter": "Details/Editar medidor\u{26A1}", + "edit.map":"Editar mapa", + "edit.meter": "Editar medidor", "edit.unit": "Editar unidad", "email": "Correo electrónico", "enable": "Activar", From 823b1614d82e36bc5bbdf1bbf40d2baa6e3831c5 Mon Sep 17 00:00:00 2001 From: Juan Gutierrez <juanjoseguva@gmail.com> Date: Thu, 18 Jul 2024 11:29:08 -0700 Subject: [PATCH 03/50] Adding licence header --- src/client/app/components/maps/CreateMapModalComponent.tsx | 4 ++++ src/client/app/components/maps/EditMapModalComponent.tsx | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/client/app/components/maps/CreateMapModalComponent.tsx b/src/client/app/components/maps/CreateMapModalComponent.tsx index bde6ab39e..a578a0ba5 100644 --- a/src/client/app/components/maps/CreateMapModalComponent.tsx +++ b/src/client/app/components/maps/CreateMapModalComponent.tsx @@ -1,3 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + import * as React from 'react'; import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; diff --git a/src/client/app/components/maps/EditMapModalComponent.tsx b/src/client/app/components/maps/EditMapModalComponent.tsx index b1054ccca..012388827 100644 --- a/src/client/app/components/maps/EditMapModalComponent.tsx +++ b/src/client/app/components/maps/EditMapModalComponent.tsx @@ -1,3 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + import * as React from 'react'; import { useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; From 09b333a934f2c422fd0deaffc9fc4def93cc0821 Mon Sep 17 00:00:00 2001 From: Juan Gutierrez <juanjoseguva@gmail.com> Date: Thu, 18 Jul 2024 11:32:54 -0700 Subject: [PATCH 04/50] Removing unused line --- .../app/components/maps/CreateMapModalComponent.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/app/components/maps/CreateMapModalComponent.tsx b/src/client/app/components/maps/CreateMapModalComponent.tsx index a578a0ba5..222113734 100644 --- a/src/client/app/components/maps/CreateMapModalComponent.tsx +++ b/src/client/app/components/maps/CreateMapModalComponent.tsx @@ -7,7 +7,6 @@ import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input } from 'reactstrap'; import { Link } from 'react-router-dom'; -import { CalibrationModeTypes } from '../../types/redux/map'; interface CreateMapModalProps { show: boolean; @@ -16,7 +15,8 @@ interface CreateMapModalProps { } /** - * + * Defines the create map modal form + * @returns Map create element */ function CreateMapModalComponent({ show, handleClose, createNewMap }: CreateMapModalProps) { const [nameInput, setNameInput] = useState(''); @@ -40,7 +40,7 @@ function CreateMapModalComponent({ show, handleClose, createNewMap }: CreateMapM <Input id="mapName" value={nameInput} - onChange={(e) => setNameInput(e.target.value)} + onChange={e => setNameInput(e.target.value)} /> </FormGroup> <FormGroup> @@ -49,7 +49,7 @@ function CreateMapModalComponent({ show, handleClose, createNewMap }: CreateMapM id="mapNote" type="textarea" value={noteInput} - onChange={(e) => setNoteInput(e.target.value)} + onChange={e => setNoteInput(e.target.value)} /> </FormGroup> </Form> From 8ff0ebe6cc924be1a133005301115070a97e9c1c Mon Sep 17 00:00:00 2001 From: Severin L <severinlight3@gmail.com> Date: Wed, 24 Jul 2024 18:45:38 +0000 Subject: [PATCH 05/50] Fix to allow saves in map modal --- .../components/maps/EditMapModalComponent.tsx | 73 +++++++++---------- .../app/components/maps/MapViewComponent.tsx | 45 ++++++------ src/client/app/redux/actions/map.ts | 48 ++++++------ src/client/app/redux/reducers/maps.ts | 3 +- 4 files changed, 83 insertions(+), 86 deletions(-) diff --git a/src/client/app/components/maps/EditMapModalComponent.tsx b/src/client/app/components/maps/EditMapModalComponent.tsx index 012388827..00394842d 100644 --- a/src/client/app/components/maps/EditMapModalComponent.tsx +++ b/src/client/app/components/maps/EditMapModalComponent.tsx @@ -4,11 +4,15 @@ import * as React from 'react'; import { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { ThunkDispatch } from 'redux-thunk'; import { FormattedMessage, useIntl } from 'react-intl'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input } from 'reactstrap'; -import { Link } from 'react-router-dom'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; +import { editMapDetails, submitEditedMap, removeMap } from '../../redux/actions/map'; import { showErrorNotification } from '../../utils/notifications'; +import { State } from '../../types/redux/state'; +import { AnyAction } from 'redux'; interface EditMapModalProps { show: boolean; @@ -19,42 +23,41 @@ interface EditMapModalProps { removeMap(id: number): any; } -/** - *Defines the edit maps modal form - * @param props state variables needed to define the component - * @returns Map edit element - */ -function EditMapModalComponent(props: EditMapModalProps) { - const [nameInput, setNameInput] = useState(props.map.name); - const [noteInput, setNoteInput] = useState(props.map.note || ''); - const [circleInput, setCircleInput] = useState(props.map.circleSize.toString()); - const [displayable, setDisplayable] = useState(props.map.displayable); +const EditMapModalComponent: React.FC<EditMapModalProps> = ({ show, handleClose, map, setCalibration }) => { + const dispatch: ThunkDispatch<State, void, AnyAction> = useDispatch(); + const [nameInput, setNameInput] = useState(map.name); + const [noteInput, setNoteInput] = useState(map.note || ''); + const [circleInput, setCircleInput] = useState(map.circleSize.toString()); + const [displayable, setDisplayable] = useState(map.displayable); const intl = useIntl(); const handleSave = () => { const updatedMap = { - ...props.map, + ...map, name: nameInput, note: noteInput, circleSize: parseFloat(circleInput), - displayable: displayable + displayable }; - props.editMapDetails(updatedMap); - props.handleClose(); + dispatch(editMapDetails(updatedMap)); + dispatch(submitEditedMap(updatedMap.id) as any).then(() => { + handleClose(); + }); }; const handleDelete = () => { - const consent = window.confirm(intl.formatMessage({ id: 'map.confirm.remove' }, { name: props.map.name })); + const consent = window.confirm(intl.formatMessage({ id: 'map.confirm.remove' }, { name: map.name })); if (consent) { - props.removeMap(props.map.id); - props.handleClose(); + dispatch(removeMap(map.id) as any).then(() => { + handleClose(); + }); } }; const handleCalibrationSetting = (mode: CalibrationModeTypes) => { - props.setCalibration(mode, props.map.id); - props.handleClose(); + setCalibration(mode, map.id); + handleClose(); }; const toggleCircleEdit = () => { @@ -67,8 +70,8 @@ function EditMapModalComponent(props: EditMapModalProps) { }; return ( - <Modal isOpen={props.show} toggle={props.handleClose}> - <ModalHeader toggle={props.handleClose}> + <Modal isOpen={show} toggle={handleClose}> + <ModalHeader toggle={handleClose}> <FormattedMessage id="edit.map" /> </ModalHeader> <ModalBody> @@ -114,38 +117,34 @@ function EditMapModalComponent(props: EditMapModalProps) { </Form> <div> <Label><FormattedMessage id="map.filename" /></Label> - <p>{props.map.filename}</p> - <Link to='/calibration' onClick={() => handleCalibrationSetting(CalibrationModeTypes.initiate)}> - <Button color='primary'> - <FormattedMessage id='map.upload.new.file' /> - </Button> - </Link> + <p>{map.filename}</p> + <Button color='primary' onClick={() => handleCalibrationSetting(CalibrationModeTypes.initiate)}> + <FormattedMessage id='map.upload.new.file' /> + </Button> </div> <div> <Label><FormattedMessage id="map.calibration" /></Label> <p> - <FormattedMessage id={props.map.origin && props.map.opposite ? 'map.is.calibrated' : 'map.is.not.calibrated'} /> + <FormattedMessage id={map.origin && map.opposite ? 'map.is.calibrated' : 'map.is.not.calibrated'} /> </p> - <Link to='/calibration' onClick={() => handleCalibrationSetting(CalibrationModeTypes.calibrate)}> - <Button color='primary'> - <FormattedMessage id='map.calibrate' /> - </Button> - </Link> + <Button color='primary' onClick={() => handleCalibrationSetting(CalibrationModeTypes.calibrate)}> + <FormattedMessage id='map.calibrate' /> + </Button> </div> </ModalBody> <ModalFooter> <Button color="danger" onClick={handleDelete}> <FormattedMessage id="delete.map" /> </Button> - <Button color="secondary" onClick={props.handleClose}> + <Button color="secondary" onClick={handleClose}> <FormattedMessage id="cancel" /> </Button> <Button color="primary" onClick={handleSave}> - <FormattedMessage id="done.editing" /> + <FormattedMessage id="save.all" /> </Button> </ModalFooter> </Modal> ); -} +}; export default EditMapModalComponent; \ No newline at end of file diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index 28b853638..6f5b70e08 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { useState, useEffect } from 'react'; -import { FormattedMessage} from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import { Button } from 'reactstrap'; import * as moment from 'moment'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; @@ -23,19 +23,18 @@ interface MapViewProps { removeMap(id: number): any; } -/** - * Defines the map info card - * @param props variables passed in to define - * @returns Map info card element - */ -function MapViewComponent(props: MapViewProps) { +// editMapDetails: (map: MapMetadata) => void; +// setCalibration: (mode: CalibrationModeTypes, mapID: number) => void; +// removeMap: (id: number) => void; + +const MapViewComponent: React.FC<MapViewProps> = ({ map, isEdited, isSubmitting, editMapDetails, setCalibration, removeMap }) => { const [showEditModal, setShowEditModal] = useState(false); useEffect(() => { - if (props.isEdited) { + if (isEdited) { //updateUnsavedChanges(); } - }, [props.isEdited]); + }, [isEdited]); const handleShowModal = () => setShowEditModal(true); const handleCloseModal = () => setShowEditModal(false); @@ -43,31 +42,31 @@ function MapViewComponent(props: MapViewProps) { return ( <div className="card"> <div className="identifier-container"> - {props.map.name} {props.isSubmitting ? '(Submitting)' : props.isEdited ? '(Edited)' : ''} + {map.name} {isSubmitting ? '(Submitting)' : isEdited ? '(Edited)' : ''} </div> <div className="item-container"> <b><FormattedMessage id="map.displayable" /></b> - <span style={{ color: props.map.displayable ? 'green' : 'red' }}> - <FormattedMessage id={props.map.displayable ? 'map.is.displayable' : 'map.is.not.displayable'} /> + <span style={{ color: map.displayable ? 'green' : 'red' }}> + <FormattedMessage id={map.displayable ? 'map.is.displayable' : 'map.is.not.displayable'} /> </span> </div> <div className="item-container"> - <b><FormattedMessage id="map.circle.size" /></b> {props.map.circleSize} + <b><FormattedMessage id="map.circle.size" /></b> {map.circleSize} </div> <div className="item-container"> <b><FormattedMessage id="map.modified.date" /></b> - {moment.parseZone(props.map.modifiedDate, undefined, true).format('dddd, MMM DD, YYYY hh:mm a')} + {moment.parseZone(map.modifiedDate, undefined, true).format('dddd, MMM DD, YYYY hh:mm a')} </div> <div className="item-container"> - <b><FormattedMessage id="map.filename" /></b> {props.map.filename} + <b><FormattedMessage id="map.filename" /></b> {map.filename} </div> <div className="item-container"> - <b><FormattedMessage id="note" /></b> {props.map.note} + <b><FormattedMessage id="note" /></b> {map.note} </div> <div className="item-container"> <b><FormattedMessage id="map.calibration" /></b> - <span style={{ color: props.map.origin && props.map.opposite ? 'black' : 'gray' }}> - <FormattedMessage id={props.map.origin && props.map.opposite ? 'map.is.calibrated' : 'map.is.not.calibrated'} /> + <span style={{ color: map.origin && map.opposite ? 'black' : 'gray' }}> + <FormattedMessage id={map.origin && map.opposite ? 'map.is.calibrated' : 'map.is.not.calibrated'} /> </span> </div> {hasToken() && ( @@ -80,13 +79,13 @@ function MapViewComponent(props: MapViewProps) { <EditMapModalComponent show={showEditModal} handleClose={handleCloseModal} - map={props.map} - editMapDetails={props.editMapDetails} - setCalibration={props.setCalibration} - removeMap={props.removeMap} + map={map} + setCalibration={setCalibration} + editMapDetails={editMapDetails} + removeMap={removeMap} /> </div> ); -} +}; export default MapViewComponent; \ No newline at end of file diff --git a/src/client/app/redux/actions/map.ts b/src/client/app/redux/actions/map.ts index 8fb9d33aa..27bc4b47a 100644 --- a/src/client/app/redux/actions/map.ts +++ b/src/client/app/redux/actions/map.ts @@ -2,24 +2,20 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import {ActionType, Dispatch, GetState, Thunk} from '../../types/redux/actions'; +import { ActionType, Dispatch, GetState, Thunk } from '../../types/redux/actions'; import * as t from '../../types/redux/map'; -import {CalibrationModeTypes, MapData, MapMetadata} from '../../types/redux/map'; -import { - calibrate, - CalibratedPoint, - CalibrationResult, - CartesianPoint, - Dimensions, - GPSPoint -} from '../../utils/calibration'; -import {State} from '../../types/redux/state'; -import {mapsApi} from '../../utils/api'; -import {showErrorNotification, showSuccessNotification} from '../../utils/notifications'; +import { CalibrationModeTypes, MapData, MapMetadata } from '../../types/redux/map'; +import { calibrate, CalibratedPoint, CalibrationResult, CartesianPoint, Dimensions, GPSPoint } from '../../utils/calibration'; +import { State } from '../../types/redux/state'; +import MapsApi from '../../utils/api/MapsApi'; +import ApiBackend from '../../utils/api/ApiBackend'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; import * as moment from 'moment'; -import {browserHistory} from '../../utils/history'; -import {logToServer} from './logs'; +import { browserHistory } from '../../utils/history'; +import { logToServer } from './logs'; + +const mapsApi = new MapsApi(new ApiBackend()); function requestMapsDetails(): t.RequestMapsDetailsAction { return { type: ActionType.RequestMapsDetails }; @@ -34,23 +30,27 @@ function submitMapEdits(mapID: number): t.SubmitEditedMapAction { } function confirmMapEdits(mapID: number): t.ConfirmEditedMapAction { - return { type: ActionType.ConfirmEditedMap, mapID}; + return { type: ActionType.ConfirmEditedMap, mapID }; } export function fetchMapsDetails(): Thunk { return async (dispatch: Dispatch) => { dispatch(requestMapsDetails()); - const mapsDetails = await mapsApi.details(); - dispatch(receiveMapsDetails(mapsDetails)); + try { + const mapsDetails = await mapsApi.details(); + dispatch(receiveMapsDetails(mapsDetails)); + } catch (error) { + showErrorNotification(translate('failed.to.fetch.maps')); + } }; } export function editMapDetails(map: MapMetadata): t.EditMapDetailsAction { - return {type: ActionType.EditMapDetails, map}; + return { type: ActionType.EditMapDetails, map }; } function incrementCounter(): t.IncrementCounterAction { - return { type: ActionType.IncrementCounter}; + return { type: ActionType.IncrementCounter }; } export function setNewMap(): Thunk { @@ -103,7 +103,7 @@ export function dropCalibration(): Thunk { } function resetCalibration(mapToReset: number): t.ResetCalibrationAction { - return { type: ActionType.ResetCalibration, mapID: mapToReset}; + return { type: ActionType.ResetCalibration, mapID: mapToReset }; } export function updateMapSource(data: MapMetadata): t.UpdateMapSourceAction { @@ -163,7 +163,7 @@ function hasCartesian(point: CalibratedPoint) { } function updateCalibrationSet(calibratedPoint: CalibratedPoint): t.AppendCalibrationSetAction { - return { type: ActionType.AppendCalibrationSet, calibratedPoint}; + return { type: ActionType.AppendCalibrationSet, calibratedPoint }; } /** @@ -199,11 +199,11 @@ function prepareDataToCalculation(state: State): CalibrationResult { } function updateResult(result: CalibrationResult): t.UpdateCalibrationResultAction { - return { type: ActionType.UpdateCalibrationResults, result}; + return { type: ActionType.UpdateCalibrationResults, result }; } export function resetCurrentPoint(): t.ResetCurrentPointAction { - return { type: ActionType.ResetCurrentPoint } ; + return { type: ActionType.ResetCurrentPoint }; } export function submitEditedMaps(): Thunk { diff --git a/src/client/app/redux/reducers/maps.ts b/src/client/app/redux/reducers/maps.ts index 82ffb055c..be63ba71e 100644 --- a/src/client/app/redux/reducers/maps.ts +++ b/src/client/app/redux/reducers/maps.ts @@ -157,10 +157,10 @@ export default function maps(state = defaultState, action: MapsAction) { }; case ActionType.ConfirmEditedMap: submitting = state.submitting; + submitting.splice(submitting.indexOf(action.mapID), 1); byMapID = state.byMapID; editedMaps = state.editedMaps; if (action.mapID > 0) { - submitting.splice(submitting.indexOf(action.mapID)); byMapID[action.mapID] = { ...editedMaps[action.mapID] }; } delete editedMaps[action.mapID]; @@ -232,7 +232,6 @@ export default function maps(state = defaultState, action: MapsAction) { return { ...state, editedMaps: { - ...state.editedMaps, [calibrated]: { ...state.editedMaps[calibrated], calibrationResult: action.result From dc87b561c7155ada5c850a269da977b06d154a87 Mon Sep 17 00:00:00 2001 From: hazeltonbw <hazeltonbw@gmail.com> Date: Sun, 28 Jul 2024 01:36:34 -0700 Subject: [PATCH 06/50] Create and utilize new maps selector --- .../app/components/maps/MapsDetailComponent.tsx | 3 +++ src/client/app/redux/reducers/maps.ts | 4 +--- src/client/app/redux/selectors/maps.ts | 13 +++++++++++++ src/client/app/redux/selectors/uiSelectors.ts | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 src/client/app/redux/selectors/maps.ts diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index d154133da..0d6d96772 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -11,6 +11,9 @@ import MapViewContainer from '../../containers/maps/MapViewContainer'; import { hasToken } from '../../utils/token'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import '../../styles/card-page.css'; +import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; +import { selectMaps } from '../../redux/selectors/maps'; + const maps: number[] = useAppSelector(selectMaps); interface MapsDetailProps { maps: number[]; diff --git a/src/client/app/redux/reducers/maps.ts b/src/client/app/redux/reducers/maps.ts index be63ba71e..3f5ce4253 100644 --- a/src/client/app/redux/reducers/maps.ts +++ b/src/client/app/redux/reducers/maps.ts @@ -6,7 +6,6 @@ import { MapMetadata, MapsAction, MapState } from '../../types/redux/map'; import { ActionType } from '../../types/redux/actions'; import { keyBy } from 'lodash'; import { CalibratedPoint } from '../../utils/calibration'; -import { RootState } from '../../store'; const defaultState: MapState = { isLoading: false, @@ -241,5 +240,4 @@ export default function maps(state = defaultState, action: MapsAction) { default: return state; } -} -export const selectMapState = (state: RootState) => state.maps; \ No newline at end of file +}; \ No newline at end of file diff --git a/src/client/app/redux/selectors/maps.ts b/src/client/app/redux/selectors/maps.ts new file mode 100644 index 000000000..0d208a1ca --- /dev/null +++ b/src/client/app/redux/selectors/maps.ts @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { RootState } from "store"; +import { createSelector } from "@reduxjs/toolkit"; + +export const selectMapState = (state: RootState) => state.maps; +export const selectMaps = createSelector([selectMapState], maps => { + return Object.keys(maps.byMapID) + .map(key => parseInt(key)) + .filter(key => !isNaN(key)); +}); \ No newline at end of file diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 6baecc5db..0eeff547a 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -19,7 +19,7 @@ import { } from '../../utils/calibration'; import { metersInGroup, unitsCompatibleWithMeters } from '../../utils/determineCompatibleUnits'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { selectMapState } from '../reducers/maps'; +import { selectMapState } from '../selectors/maps'; import { selectChartToRender, selectGraphAreaNormalization, selectGraphState, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit, selectSliderRangeInterval From c799c95f13330f25a9483376705fa99811f4b3cf Mon Sep 17 00:00:00 2001 From: hazeltonbw <hazeltonbw@gmail.com> Date: Sun, 28 Jul 2024 01:46:24 -0700 Subject: [PATCH 07/50] Class Component to Redux function component update --- src/client/app/components/RouteComponent.tsx | 4 +- .../components/maps/MapsDetailComponent.tsx | 114 +++++++----------- 2 files changed, 46 insertions(+), 72 deletions(-) diff --git a/src/client/app/components/RouteComponent.tsx b/src/client/app/components/RouteComponent.tsx index ddeb46469..42d8401b5 100644 --- a/src/client/app/components/RouteComponent.tsx +++ b/src/client/app/components/RouteComponent.tsx @@ -6,7 +6,7 @@ import { IntlProvider } from 'react-intl'; import { RouterProvider, createBrowserRouter } from 'react-router-dom'; import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; -import MapsDetailContainer from '../containers/maps/MapsDetailContainer'; +import MapsDetailComponent from './maps/MapsDetailComponent'; import { useAppSelector } from '../redux/reduxHooks'; import LocaleTranslationData from '../translations/data'; import { UserRole } from '../types/items'; @@ -56,7 +56,7 @@ const router = createBrowserRouter([ children: [ { path: 'admin', element: <AdminComponent /> }, { path: 'calibration', element: <MapCalibrationContainer /> }, - { path: 'maps', element: <MapsDetailContainer /> }, + { path: 'maps', element: <MapsDetailComponent /> }, { path: 'users/new', element: <CreateUserComponent /> }, { path: 'units', element: <UnitsDetailComponent /> }, { path: 'conversions', element: <ConversionsDetailComponent /> }, diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index 0d6d96772..7a06e272c 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -11,79 +11,53 @@ import MapViewContainer from '../../containers/maps/MapViewContainer'; import { hasToken } from '../../utils/token'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import '../../styles/card-page.css'; +import { fetchMapsDetails, setNewMap, submitEditedMaps } from '../../redux/actions/map'; import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; import { selectMaps } from '../../redux/selectors/maps'; - const maps: number[] = useAppSelector(selectMaps); - -interface MapsDetailProps { - maps: number[]; - unsavedChanges: boolean; - fetchMapsDetails(): Promise<any>; - submitEditedMaps(): Promise<any>; - createNewMap(): any; -} - -export default class MapsDetailComponent extends React.Component<MapsDetailProps> { - constructor(props: MapsDetailProps) { - super(props); - this.handleSubmitClicked = this.handleSubmitClicked.bind(this); - } - - public componentDidMount() { - this.props.fetchMapsDetails(); - } +import { AppDispatch } from 'store'; - public render() { - return ( - <div className='flexGrowOne'> - <TooltipHelpComponent page='maps' /> - <div className='container-fluid'> - <h2 style={titleStyle}> - <FormattedMessage id='maps' /> - <div style={tooltipStyle}> - <TooltipMarkerComponent page='maps' helpTextId='help.admin.mapview' /> - </div> - </h2> - <div className="edit-btn"> - <Link to='/calibration' onClick={() => this.props.createNewMap()}> - <Button color='primary'> - <FormattedMessage id='create.map' /> - </Button> - </Link> - </div> - <div className="card-container"> - {this.props.maps.map(mapID => ( - <MapViewContainer key={mapID} id={mapID} /> - ))} +export default function MapsDetailComponent() { + const dispatch: AppDispatch = useAppDispatch(); + // Load map IDs from state and store in number array + const maps: number[] = useAppSelector(selectMaps); + React.useEffect(() => { + // Load maps from state on component mount (componentDidMount) + dispatch(fetchMapsDetails()); + }, []); + + return ( + <div className='flexGrowOne'> + <TooltipHelpComponent page='maps' /> + <div className='container-fluid'> + <h2 className='text-center'> + <FormattedMessage id='maps' /> + <div className='d-inline-block fs-5'> + <TooltipMarkerComponent page='maps' helpTextId='help.admin.mapview' /> </div> - {hasToken() && ( - <div className="edit-btn"> - <Button - color='success' - disabled={!this.props.unsavedChanges} - onClick={this.handleSubmitClicked} - > - <FormattedMessage id='save.map.edits' /> - </Button> - </div> - )} + </h2> + <div className="edit-btn"> + <Link to='/calibration' onClick={() => dispatch(setNewMap())}> + <Button color='primary'> + <FormattedMessage id='create.map' /> + </Button> + </Link> </div> + <div className="card-container"> + {maps.map(mapID => ( + <MapViewContainer key={mapID} id={mapID} /> + ))} + </div> + {hasToken() && ( + <div className="edit-btn"> + <Button + color='success' + onClick={() => dispatch(submitEditedMaps())} + > + <FormattedMessage id='save.map.edits' /> + </Button> + </div> + )} </div> - ); - } - - private handleSubmitClicked() { - this.props.submitEditedMaps(); - // Notify that the unsaved changes have been submitted - // this.removeUnsavedChanges(); - } -} - -const titleStyle: React.CSSProperties = { - textAlign: 'center' -}; - -const tooltipStyle = { - display: 'inline-block', - fontSize: '50%' -}; \ No newline at end of file + </div> + ); +} \ No newline at end of file From 0a941daa48913d86541c3d361d4c6b2d74a00e40 Mon Sep 17 00:00:00 2001 From: Juan Gutierrez <juanjoseguva@gmail.com> Date: Wed, 24 Jul 2024 16:12:43 -0700 Subject: [PATCH 08/50] Adding ": " to map card descriptors for formatting --- src/client/app/translations/data.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 213651a02..e8378e3be 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -302,11 +302,11 @@ const LocaleTranslationData = { "map.bad.digita": "Greater than 360, please change angle to a number between 0 and 360", "map.bad.digitb": "Less than 0, please change angle to a number between 0 and 360", "map.calibrate": "Calibrate", - "map.calibration": "Calibration status", - "map.circle.size": "Map Circle Size", + "map.calibration": "Calibration status: ", + "map.circle.size": "Map Circle Size: ", "map.confirm.remove": "Are you sure you want to remove map", "map.displayable": "Map Display", - "map.filename": "Map file", + "map.filename": "Map File: ", "map.id": "Map ID", "map.interval": "Map Interval", "map.is.calibrated": "Calibration Complete", @@ -315,7 +315,7 @@ const LocaleTranslationData = { "map.is.not.calibrated": "Calibration Needed", "map.is.not.displayable": "Display Disabled", "map.load.complete": "Map load complete from", - "map.modified.date": "Last Modified", + "map.modified.date": "Last Modified: ", "map.name": "Map Name", "map.new.angle": "List the angle with respect to true north (0 to 360)", "map.new.name": "Define a name for the map:", @@ -799,11 +799,11 @@ const LocaleTranslationData = { "map.bad.digita": "Supérieur à 360, veuillez changer l'angle en un nombre compris entre 0 et 360", "map.bad.digitb": "Moins de 0, veuillez changer l'angle en un nombre compris entre 0 et 360", "map.calibrate": "Étalonner", - "map.calibration": "Statut d'étalonnage", - "map.circle.size": "Taille du cercle de la carte", + "map.calibration": "Statut d'étalonnage: ", + "map.circle.size": "Taille du cercle de la carte: ", "map.confirm.remove": "Voulez-vous vraiment supprimer la carte?", - "map.displayable": "Affichage de la carte", - "map.filename": "Fichier de carte", + "map.displayable": "Affichage de la carte: ", + "map.filename": "Fichier de carte: ", "map.id": "ID de la carte", "map.interval": "Intervalle de carte", "map.is.calibrated": "Étalonnage terminé", @@ -812,7 +812,7 @@ const LocaleTranslationData = { "map.is.not.calibrated": "Étalonnage nécessaire", "map.is.not.displayable": "Affichage désactivé", "map.load.complete": "Chargement de la carte terminé à partir de", - "map.modified.date": "Dernière modification", + "map.modified.date": "Dernière modification: ", "map.name": "Nom de la carte", "map.new.angle": "Indiquez l'angle par rapport au vrai nord (0 à 360)", "map.new.name": "Définir un nom pour la carte", @@ -1296,11 +1296,11 @@ const LocaleTranslationData = { "map.bad.digita": "Mayor a 360, por favor cambiar el angúlo a un número entre 0 a 360", "map.bad.digitb": "Menor a 0, por favor cambiar el angúlo a un número entre 0 a 360", "map.calibrate": "Calibrar", - "map.calibration": "Estado de calibración", - "map.circle.size": "Tamaño del círculo en el mapa", + "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.displayable": "Visuilización del mapa", - "map.filename": "Archivo del mapa", + "map.displayable": "Visuilización del mapa: ", + "map.filename": "Archivo del mapa: ", "map.id": "ID del Mapa", "map.interval": "Intervalo de mapa", "map.is.calibrated": "Calibración completa", @@ -1309,7 +1309,7 @@ const LocaleTranslationData = { "map.is.not.calibrated": "Necesita calibración", "map.is.not.displayable": "Visualización desactivada", "map.load.complete": "Carga de mapas completada de", - "map.modified.date": "Última modificación", + "map.modified.date": "Última modificación: ", "map.name": "Nombre del mapa", "map.new.angle": " Liste el ángulo con respecto al norte verdadero (0 a 360)", "map.new.name": "Defina un nombre para el mapa:", From af01ec7397d21a13197a57c43e66cfd80b05bec7 Mon Sep 17 00:00:00 2001 From: Juan Gutierrez <juanjoseguva@gmail.com> Date: Wed, 24 Jul 2024 16:30:23 -0700 Subject: [PATCH 09/50] Removing unnecessary button --- src/client/app/components/maps/MapsDetailComponent.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index 7a06e272c..fad19d1e3 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -8,7 +8,6 @@ import { Link } from 'react-router-dom'; import { Button } from 'reactstrap'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import MapViewContainer from '../../containers/maps/MapViewContainer'; -import { hasToken } from '../../utils/token'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import '../../styles/card-page.css'; import { fetchMapsDetails, setNewMap, submitEditedMaps } from '../../redux/actions/map'; From b55b6724cb64998b0c637f7d7d4041be312fd822 Mon Sep 17 00:00:00 2001 From: Juan Gutierrez <juanjoseguva@gmail.com> Date: Tue, 30 Jul 2024 09:26:39 -0700 Subject: [PATCH 10/50] Adding missing : on a map descriptor --- src/client/app/components/maps/MapsDetailComponent.tsx | 10 ---------- src/client/app/translations/data.ts | 4 ++-- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index fad19d1e3..d5cebb026 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -46,16 +46,6 @@ export default function MapsDetailComponent() { <MapViewContainer key={mapID} id={mapID} /> ))} </div> - {hasToken() && ( - <div className="edit-btn"> - <Button - color='success' - onClick={() => dispatch(submitEditedMaps())} - > - <FormattedMessage id='save.map.edits' /> - </Button> - </div> - )} </div> </div> ); diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index e8378e3be..2b5e2a486 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -305,7 +305,7 @@ const LocaleTranslationData = { "map.calibration": "Calibration status: ", "map.circle.size": "Map Circle Size: ", "map.confirm.remove": "Are you sure you want to remove map", - "map.displayable": "Map Display", + "map.displayable": "Map Display: ", "map.filename": "Map File: ", "map.id": "Map ID", "map.interval": "Map Interval", @@ -808,7 +808,7 @@ const LocaleTranslationData = { "map.interval": "Intervalle de carte", "map.is.calibrated": "Étalonnage terminé", "map.is.deleted": "Carte supprimée de la base de données", - "map.is.displayable": "Affichage activé", + "map.is.displayable": "Affichage activé: ", "map.is.not.calibrated": "Étalonnage nécessaire", "map.is.not.displayable": "Affichage désactivé", "map.load.complete": "Chargement de la carte terminé à partir de", From 2b3885dbc3b0cda87fb12d46f93b8bad5b471312 Mon Sep 17 00:00:00 2001 From: Juan Gutierrez <juanjoseguva@gmail.com> Date: Tue, 30 Jul 2024 10:31:05 -0700 Subject: [PATCH 11/50] Removing item name from descriptor (so no map) --- src/client/app/translations/data.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 2b5e2a486..8a79eb692 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -303,10 +303,10 @@ const LocaleTranslationData = { "map.bad.digitb": "Less than 0, please change angle to a number between 0 and 360", "map.calibrate": "Calibrate", "map.calibration": "Calibration status: ", - "map.circle.size": "Map Circle Size: ", + "map.circle.size": "Circle Size: ", "map.confirm.remove": "Are you sure you want to remove map", - "map.displayable": "Map Display: ", - "map.filename": "Map File: ", + "map.displayable": "Display: ", + "map.filename": "File: ", "map.id": "Map ID", "map.interval": "Map Interval", "map.is.calibrated": "Calibration Complete", @@ -800,10 +800,10 @@ const LocaleTranslationData = { "map.bad.digitb": "Moins de 0, veuillez changer l'angle en un nombre compris entre 0 et 360", "map.calibrate": "Étalonner", "map.calibration": "Statut d'étalonnage: ", - "map.circle.size": "Taille du cercle de la carte: ", + "map.circle.size": "Taille du cercle: ", "map.confirm.remove": "Voulez-vous vraiment supprimer la carte?", - "map.displayable": "Affichage de la carte: ", - "map.filename": "Fichier de carte: ", + "map.displayable": "Affichage: ", + "map.filename": "Fichier: ", "map.id": "ID de la carte", "map.interval": "Intervalle de carte", "map.is.calibrated": "Étalonnage terminé", @@ -1297,10 +1297,10 @@ const LocaleTranslationData = { "map.bad.digitb": "Menor a 0, por favor cambiar el angúlo a un número entre 0 a 360", "map.calibrate": "Calibrar", "map.calibration": "Estado de calibración: ", - "map.circle.size": "Tamaño del círculo en el mapa: ", + "map.circle.size": "Tamaño del círculo: ", "map.confirm.remove": "¿Estás seguro de que quieres eliminar el mapa", - "map.displayable": "Visuilización del mapa: ", - "map.filename": "Archivo del mapa: ", + "map.displayable": "Visuilización: ", + "map.filename": "Archivo: ", "map.id": "ID del Mapa", "map.interval": "Intervalo de mapa", "map.is.calibrated": "Calibración completa", From a7e6ddf456ef3b236a4e7a666c8e069c4341d1ef Mon Sep 17 00:00:00 2001 From: root <root@Phi.lan> Date: Tue, 30 Jul 2024 23:24:35 -0700 Subject: [PATCH 12/50] Limiting map note to 30 characters. --- src/client/app/components/maps/EditMapModalComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/app/components/maps/EditMapModalComponent.tsx b/src/client/app/components/maps/EditMapModalComponent.tsx index 00394842d..462665477 100644 --- a/src/client/app/components/maps/EditMapModalComponent.tsx +++ b/src/client/app/components/maps/EditMapModalComponent.tsx @@ -111,7 +111,7 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ show, handleClose, id="mapNote" type="textarea" value={noteInput} - onChange={e => setNoteInput(e.target.value)} + onChange={e => setNoteInput(e.target.value.slice(0,30))} /> </FormGroup> </Form> From 6397225bc53603d722ea7249e9ce138741cb91a7 Mon Sep 17 00:00:00 2001 From: root <root@Phi.lan> Date: Tue, 30 Jul 2024 23:46:12 -0700 Subject: [PATCH 13/50] Treating circle size as a positive number --- src/client/app/components/maps/EditMapModalComponent.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/app/components/maps/EditMapModalComponent.tsx b/src/client/app/components/maps/EditMapModalComponent.tsx index 462665477..2e14fcbdb 100644 --- a/src/client/app/components/maps/EditMapModalComponent.tsx +++ b/src/client/app/components/maps/EditMapModalComponent.tsx @@ -100,8 +100,10 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ show, handleClose, <Label for="mapCircleSize"><FormattedMessage id="map.circle.size" /></Label> <Input id="mapCircleSize" + type='number' value={circleInput} onChange={e => setCircleInput(e.target.value)} + invalid={parseFloat(circleInput)<0} onBlur={toggleCircleEdit} /> </FormGroup> From 0fb3b4523c47a54b56ceba19da741f74062a2bf2 Mon Sep 17 00:00:00 2001 From: root <root@Phi.lan> Date: Tue, 30 Jul 2024 23:50:54 -0700 Subject: [PATCH 14/50] Changing the order of descriptors on card to match the modal. --- src/client/app/components/maps/MapViewComponent.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index 6f5b70e08..bc9a3229e 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -54,14 +54,14 @@ const MapViewComponent: React.FC<MapViewProps> = ({ map, isEdited, isSubmitting, <b><FormattedMessage id="map.circle.size" /></b> {map.circleSize} </div> <div className="item-container"> - <b><FormattedMessage id="map.modified.date" /></b> - {moment.parseZone(map.modifiedDate, undefined, true).format('dddd, MMM DD, YYYY hh:mm a')} + <b><FormattedMessage id="note" /></b> {map.note} </div> <div className="item-container"> <b><FormattedMessage id="map.filename" /></b> {map.filename} </div> <div className="item-container"> - <b><FormattedMessage id="note" /></b> {map.note} + <b><FormattedMessage id="map.modified.date" /></b> + {moment.parseZone(map.modifiedDate, undefined, true).format('dddd, MMM DD, YYYY hh:mm a')} </div> <div className="item-container"> <b><FormattedMessage id="map.calibration" /></b> From 76ddde9b96340b116a760936e0494e82efe4c146 Mon Sep 17 00:00:00 2001 From: Juan Gutierrez <juanjoseguva@gmail.com> Date: Tue, 30 Jul 2024 23:58:08 -0700 Subject: [PATCH 15/50] Removing unused variable --- src/client/app/components/maps/MapsDetailComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index d5cebb026..4b92bc2b8 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -10,7 +10,7 @@ import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import MapViewContainer from '../../containers/maps/MapViewContainer'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import '../../styles/card-page.css'; -import { fetchMapsDetails, setNewMap, submitEditedMaps } from '../../redux/actions/map'; +import { fetchMapsDetails, setNewMap} from '../../redux/actions/map'; import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; import { selectMaps } from '../../redux/selectors/maps'; import { AppDispatch } from 'store'; From 815f9989ff813c4a1803a78914307c905f2701b7 Mon Sep 17 00:00:00 2001 From: root <root@Phi.lan> Date: Wed, 31 Jul 2024 00:06:50 -0700 Subject: [PATCH 16/50] Changing the name of the button to upload a new map image file. --- src/client/app/translations/data.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 8a79eb692..58a339921 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -322,7 +322,7 @@ const LocaleTranslationData = { "map.new.submit": "Save and continue", "map.new.upload": "Upload map image to begin.", "map.notify.calibration.needed": "Calibration needed before display", - "map.upload.new.file": "Redo", + "map.upload.new.file": "Upload New File", "max": "max", "menu": "Menu", "meter": "Meter", @@ -819,7 +819,7 @@ const LocaleTranslationData = { "map.new.submit": "Sauvegarder et continuer", "map.new.upload": "Téléchargez l'image de la carte pour commencer.", "map.notify.calibration.needed": "Étalonnage nécessaire pour l'affichage", - "map.upload.new.file": "Refaire", + "map.upload.new.file": "Upload New File\u{26A1}", "max": "max\u{26A1}", "menu": "Menu", "meter": "Mèter", @@ -1316,7 +1316,7 @@ const LocaleTranslationData = { "map.new.submit": "Guardar y continuar", "map.new.upload": "Subir la imagen del mapa para empezar.", "map.notify.calibration.needed": "Necesita calibración antes de visualizar", - "map.upload.new.file": "Rehacer", + "map.upload.new.file": "Subir un nuevo archivo", "max": "máximo", "menu": "Menú", "meter": "Medidor", From b427cbf3ff60055756c1db820e8059fd71ca44fc Mon Sep 17 00:00:00 2001 From: root <root@Phi.lan> Date: Wed, 31 Jul 2024 00:16:13 -0700 Subject: [PATCH 17/50] Resolving syntax and jsdoc declaration requirements --- src/client/app/components/maps/CreateMapModalComponent.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/app/components/maps/CreateMapModalComponent.tsx b/src/client/app/components/maps/CreateMapModalComponent.tsx index 222113734..213113abf 100644 --- a/src/client/app/components/maps/CreateMapModalComponent.tsx +++ b/src/client/app/components/maps/CreateMapModalComponent.tsx @@ -16,6 +16,10 @@ interface CreateMapModalProps { /** * Defines the create map modal form + * @param root0 + * @param root0.show + * @param root0.handleClose + * @param root0.createNewMap * @returns Map create element */ function CreateMapModalComponent({ show, handleClose, createNewMap }: CreateMapModalProps) { From b6875697eb5576a90a0e5e20e84c817dbaab837f Mon Sep 17 00:00:00 2001 From: root <root@Phi.lan> Date: Wed, 31 Jul 2024 00:19:42 -0700 Subject: [PATCH 18/50] Resolving syntax and jsdoc declaration requirements --- src/client/app/components/maps/MapsDetailComponent.tsx | 4 ++++ src/client/app/redux/reducers/maps.ts | 2 +- src/client/app/redux/selectors/maps.ts | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index 4b92bc2b8..18d231643 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -15,6 +15,10 @@ import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; import { selectMaps } from '../../redux/selectors/maps'; import { AppDispatch } from 'store'; +/** + * Defines the maps page card view + * @returns Maps page element + */ export default function MapsDetailComponent() { const dispatch: AppDispatch = useAppDispatch(); // Load map IDs from state and store in number array diff --git a/src/client/app/redux/reducers/maps.ts b/src/client/app/redux/reducers/maps.ts index 3f5ce4253..714980d5d 100644 --- a/src/client/app/redux/reducers/maps.ts +++ b/src/client/app/redux/reducers/maps.ts @@ -240,4 +240,4 @@ export default function maps(state = defaultState, action: MapsAction) { default: return state; } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/client/app/redux/selectors/maps.ts b/src/client/app/redux/selectors/maps.ts index 0d208a1ca..d1c0c5c3e 100644 --- a/src/client/app/redux/selectors/maps.ts +++ b/src/client/app/redux/selectors/maps.ts @@ -2,12 +2,12 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { RootState } from "store"; -import { createSelector } from "@reduxjs/toolkit"; +import { RootState } from 'store'; +import { createSelector } from '@reduxjs/toolkit'; export const selectMapState = (state: RootState) => state.maps; export const selectMaps = createSelector([selectMapState], maps => { return Object.keys(maps.byMapID) - .map(key => parseInt(key)) - .filter(key => !isNaN(key)); + .map(key => parseInt(key)) + .filter(key => !isNaN(key)); }); \ No newline at end of file From d57da8c46cc97b71d7ad7618f97899332d091d57 Mon Sep 17 00:00:00 2001 From: hazeltonbw <hazeltonbw@gmail.com> Date: Wed, 31 Jul 2024 01:27:35 -0700 Subject: [PATCH 19/50] use useAppDispatch for type safety --- src/client/app/components/maps/EditMapModalComponent.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/client/app/components/maps/EditMapModalComponent.tsx b/src/client/app/components/maps/EditMapModalComponent.tsx index 2e14fcbdb..36aa92bfb 100644 --- a/src/client/app/components/maps/EditMapModalComponent.tsx +++ b/src/client/app/components/maps/EditMapModalComponent.tsx @@ -4,15 +4,13 @@ import * as React from 'react'; import { useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { ThunkDispatch } from 'redux-thunk'; import { FormattedMessage, useIntl } from 'react-intl'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input } from 'reactstrap'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; import { editMapDetails, submitEditedMap, removeMap } from '../../redux/actions/map'; import { showErrorNotification } from '../../utils/notifications'; -import { State } from '../../types/redux/state'; -import { AnyAction } from 'redux'; +import { useAppDispatch } from '../../redux/reduxHooks'; +import { AppDispatch } from 'store'; interface EditMapModalProps { show: boolean; @@ -24,7 +22,7 @@ interface EditMapModalProps { } const EditMapModalComponent: React.FC<EditMapModalProps> = ({ show, handleClose, map, setCalibration }) => { - const dispatch: ThunkDispatch<State, void, AnyAction> = useDispatch(); + const dispatch: AppDispatch = useAppDispatch(); const [nameInput, setNameInput] = useState(map.name); const [noteInput, setNoteInput] = useState(map.note || ''); const [circleInput, setCircleInput] = useState(map.circleSize.toString()); From bc49a65c3a878625fdbd7a7ae17e0c29eee85ea9 Mon Sep 17 00:00:00 2001 From: hazeltonbw <hazeltonbw@gmail.com> Date: Wed, 31 Jul 2024 01:30:05 -0700 Subject: [PATCH 20/50] Remove container props, utilize dispatch for redux actions --- .../components/maps/EditMapModalComponent.tsx | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/client/app/components/maps/EditMapModalComponent.tsx b/src/client/app/components/maps/EditMapModalComponent.tsx index 36aa92bfb..d904da1e5 100644 --- a/src/client/app/components/maps/EditMapModalComponent.tsx +++ b/src/client/app/components/maps/EditMapModalComponent.tsx @@ -7,7 +7,7 @@ import { useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input } from 'reactstrap'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; -import { editMapDetails, submitEditedMap, removeMap } from '../../redux/actions/map'; +import { editMapDetails, submitEditedMap, removeMap, setCalibration } from '../../redux/actions/map'; import { showErrorNotification } from '../../utils/notifications'; import { useAppDispatch } from '../../redux/reduxHooks'; import { AppDispatch } from 'store'; @@ -16,12 +16,9 @@ interface EditMapModalProps { show: boolean; handleClose: () => void; map: MapMetadata; - editMapDetails(map: MapMetadata): any; - setCalibration(mode: CalibrationModeTypes, mapID: number): any; - removeMap(id: number): any; } -const EditMapModalComponent: React.FC<EditMapModalProps> = ({ show, handleClose, map, setCalibration }) => { +const EditMapModalComponent: React.FC<EditMapModalProps> = ({ show, handleClose, map}) => { const dispatch: AppDispatch = useAppDispatch(); const [nameInput, setNameInput] = useState(map.name); const [noteInput, setNoteInput] = useState(map.note || ''); @@ -39,22 +36,20 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ show, handleClose, displayable }; dispatch(editMapDetails(updatedMap)); - dispatch(submitEditedMap(updatedMap.id) as any).then(() => { - handleClose(); - }); + dispatch(submitEditedMap(updatedMap.id)); + handleClose(); }; const handleDelete = () => { const consent = window.confirm(intl.formatMessage({ id: 'map.confirm.remove' }, { name: map.name })); if (consent) { - dispatch(removeMap(map.id) as any).then(() => { - handleClose(); - }); + dispatch(removeMap(map.id)); + handleClose(); } }; const handleCalibrationSetting = (mode: CalibrationModeTypes) => { - setCalibration(mode, map.id); + dispatch(setCalibration(mode, map.id)); handleClose(); }; From 105395dda748000cdc2cc2636067dfd4a11d7bc3 Mon Sep 17 00:00:00 2001 From: hazeltonbw <hazeltonbw@gmail.com> Date: Wed, 31 Jul 2024 01:30:24 -0700 Subject: [PATCH 21/50] Use the new component instead of old container --- src/client/app/components/maps/MapCalibrationComponent.tsx | 5 +++-- src/client/app/components/maps/MapsDetailComponent.tsx | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/client/app/components/maps/MapCalibrationComponent.tsx b/src/client/app/components/maps/MapCalibrationComponent.tsx index 1d60d51cf..cfdbbd97c 100644 --- a/src/client/app/components/maps/MapCalibrationComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationComponent.tsx @@ -6,8 +6,9 @@ import * as React from 'react'; import MapCalibrationChartDisplayContainer from '../../containers/maps/MapCalibrationChartDisplayContainer'; import MapCalibrationInfoDisplayContainer from '../../containers/maps/MapCalibrationInfoDisplayContainer'; import MapCalibrationInitiateContainer from '../../containers/maps/MapCalibrationInitiateContainer'; -import MapsDetailContainer from '../../containers/maps/MapsDetailContainer'; +//import MapsDetailContainer from '../../containers/maps/MapsDetailContainer'; import { CalibrationModeTypes } from '../../types/redux/map'; +import MapsDetailComponent from './MapsDetailComponent'; interface MapCalibrationProps { mode: CalibrationModeTypes; @@ -47,7 +48,7 @@ export default class MapCalibrationComponent extends React.Component<MapCalibrat } else { // preview mode containers return ( <div className='container-fluid'> - <MapsDetailContainer /> + <MapsDetailComponent /> </div> ); } diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index 18d231643..957f20791 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -7,7 +7,7 @@ import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; import { Button } from 'reactstrap'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import MapViewContainer from '../../containers/maps/MapViewContainer'; +import MapViewComponent from './MapViewComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import '../../styles/card-page.css'; import { fetchMapsDetails, setNewMap} from '../../redux/actions/map'; @@ -47,7 +47,7 @@ export default function MapsDetailComponent() { </div> <div className="card-container"> {maps.map(mapID => ( - <MapViewContainer key={mapID} id={mapID} /> + <MapViewComponent key={mapID} mapID={mapID} /> ))} </div> </div> From 0d0a421db3e34b4800833404e7c23a6796102472 Mon Sep 17 00:00:00 2001 From: hazeltonbw <hazeltonbw@gmail.com> Date: Wed, 31 Jul 2024 01:35:11 -0700 Subject: [PATCH 22/50] Remove props used for container, removed isEdited and isSubmitting status as they are no longer relevant in the new modals --- .../app/components/maps/MapViewComponent.tsx | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index bc9a3229e..56d018d83 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -14,35 +14,18 @@ import EditMapModalComponent from './EditMapModalComponent'; interface MapViewProps { - id: number; - map: MapMetadata; - isEdited: boolean; - isSubmitting: boolean; - editMapDetails(map: MapMetadata): any; - setCalibration(mode: CalibrationModeTypes, mapID: number): any; - removeMap(id: number): any; + mapID: number; } -// editMapDetails: (map: MapMetadata) => void; -// setCalibration: (mode: CalibrationModeTypes, mapID: number) => void; -// removeMap: (id: number) => void; - -const MapViewComponent: React.FC<MapViewProps> = ({ map, isEdited, isSubmitting, editMapDetails, setCalibration, removeMap }) => { +const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { const [showEditModal, setShowEditModal] = useState(false); - - useEffect(() => { - if (isEdited) { - //updateUnsavedChanges(); - } - }, [isEdited]); - const handleShowModal = () => setShowEditModal(true); const handleCloseModal = () => setShowEditModal(false); return ( <div className="card"> <div className="identifier-container"> - {map.name} {isSubmitting ? '(Submitting)' : isEdited ? '(Edited)' : ''} + {map.name} </div> <div className="item-container"> <b><FormattedMessage id="map.displayable" /></b> From 75c5609d3a4a891825160d11657b8b18c50bcfd5 Mon Sep 17 00:00:00 2001 From: hazeltonbw <hazeltonbw@gmail.com> Date: Wed, 31 Jul 2024 01:37:18 -0700 Subject: [PATCH 23/50] Delete unused types and only import necessary functions from moment package --- src/client/app/components/maps/MapViewComponent.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index 56d018d83..3e95eaaa4 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -6,8 +6,7 @@ import * as React from 'react'; import { useState, useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; import { Button } from 'reactstrap'; -import * as moment from 'moment'; -import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; +import { parseZone } from 'moment'; import { hasToken } from '../../utils/token'; import '../../styles/card-page.css'; import EditMapModalComponent from './EditMapModalComponent'; @@ -44,7 +43,7 @@ const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { </div> <div className="item-container"> <b><FormattedMessage id="map.modified.date" /></b> - {moment.parseZone(map.modifiedDate, undefined, true).format('dddd, MMM DD, YYYY hh:mm a')} + {parseZone(map.modifiedDate, undefined, true).format('dddd, MMM DD, YYYY hh:mm a')} </div> <div className="item-container"> <b><FormattedMessage id="map.calibration" /></b> From 8430932f3fb4d5ea0be5e520487facc0974c776c Mon Sep 17 00:00:00 2001 From: hazeltonbw <hazeltonbw@gmail.com> Date: Wed, 31 Jul 2024 01:40:06 -0700 Subject: [PATCH 24/50] Delete unused props & functions, add new memoized selector --- src/client/app/components/maps/MapViewComponent.tsx | 12 ++++++------ src/client/app/redux/selectors/maps.ts | 10 +++++++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index 3e95eaaa4..d6282ecf8 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -3,15 +3,15 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Button } from 'reactstrap'; import { parseZone } from 'moment'; import { hasToken } from '../../utils/token'; import '../../styles/card-page.css'; import EditMapModalComponent from './EditMapModalComponent'; - - +import { makeSelectMapById } from '../../redux/selectors/maps'; +import { useSelector } from 'react-redux'; interface MapViewProps { mapID: number; } @@ -21,6 +21,9 @@ const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { const handleShowModal = () => setShowEditModal(true); const handleCloseModal = () => setShowEditModal(false); + const selectMapById = React.useMemo(makeSelectMapById, []); + const map = useSelector(state => selectMapById(state, mapID)); + return ( <div className="card"> <div className="identifier-container"> @@ -62,9 +65,6 @@ const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { show={showEditModal} handleClose={handleCloseModal} map={map} - setCalibration={setCalibration} - editMapDetails={editMapDetails} - removeMap={removeMap} /> </div> ); diff --git a/src/client/app/redux/selectors/maps.ts b/src/client/app/redux/selectors/maps.ts index d1c0c5c3e..909990754 100644 --- a/src/client/app/redux/selectors/maps.ts +++ b/src/client/app/redux/selectors/maps.ts @@ -10,4 +10,12 @@ export const selectMaps = createSelector([selectMapState], maps => { return Object.keys(maps.byMapID) .map(key => parseInt(key)) .filter(key => !isNaN(key)); -}); \ No newline at end of file +}); + +export const makeSelectMapById = () => { + const selectMapById = createSelector( + [selectMapState, (state, id) => id], + (maps, id) => maps.byMapID[id] + ); + return selectMapById; +}; \ No newline at end of file From 6db61deb2f6222eec93869b567a4848cdd908985 Mon Sep 17 00:00:00 2001 From: Juan Gutierrez <juanjoseguva@gmail.com> Date: Wed, 31 Jul 2024 19:43:05 -0700 Subject: [PATCH 25/50] Treating map file name as an uneditable item --- src/client/app/components/maps/EditMapModalComponent.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/client/app/components/maps/EditMapModalComponent.tsx b/src/client/app/components/maps/EditMapModalComponent.tsx index d904da1e5..77efc860e 100644 --- a/src/client/app/components/maps/EditMapModalComponent.tsx +++ b/src/client/app/components/maps/EditMapModalComponent.tsx @@ -112,7 +112,13 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ show, handleClose, </Form> <div> <Label><FormattedMessage id="map.filename" /></Label> - <p>{map.filename}</p> + <Input + id='mapFilename' + name='mapFilename' + type='text' + defaultValue={map.filename} + disabled> + </Input> <Button color='primary' onClick={() => handleCalibrationSetting(CalibrationModeTypes.initiate)}> <FormattedMessage id='map.upload.new.file' /> </Button> From e775c62d9e4c3bf0ffb2fe7cb02e385aeab0ab3b Mon Sep 17 00:00:00 2001 From: Juan Gutierrez <juanjoseguva@gmail.com> Date: Thu, 1 Aug 2024 08:38:49 -0700 Subject: [PATCH 26/50] Handling displayable with an enum instead of a boolean and fixing a typo in the translations. --- .../components/maps/EditMapModalComponent.tsx | 23 +++++++++++-------- src/client/app/translations/data.ts | 2 +- src/client/app/types/redux/map.ts | 7 +++++- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/client/app/components/maps/EditMapModalComponent.tsx b/src/client/app/components/maps/EditMapModalComponent.tsx index 77efc860e..8eb543da7 100644 --- a/src/client/app/components/maps/EditMapModalComponent.tsx +++ b/src/client/app/components/maps/EditMapModalComponent.tsx @@ -8,6 +8,7 @@ import { FormattedMessage, useIntl } from 'react-intl'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input } from 'reactstrap'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; import { editMapDetails, submitEditedMap, removeMap, setCalibration } from '../../redux/actions/map'; +import { DisplayableType } from '../../types/redux/map'; import { showErrorNotification } from '../../utils/notifications'; import { useAppDispatch } from '../../redux/reduxHooks'; import { AppDispatch } from 'store'; @@ -23,7 +24,7 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ show, handleClose, const [nameInput, setNameInput] = useState(map.name); const [noteInput, setNoteInput] = useState(map.note || ''); const [circleInput, setCircleInput] = useState(map.circleSize.toString()); - const [displayable, setDisplayable] = useState(map.displayable); + const [displayable, setDisplayable] = useState<DisplayableType>(map.displayable); const intl = useIntl(); @@ -33,7 +34,7 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ show, handleClose, name: nameInput, note: noteInput, circleSize: parseFloat(circleInput), - displayable + displayable: displayable as DisplayableType }; dispatch(editMapDetails(updatedMap)); dispatch(submitEditedMap(updatedMap.id)); @@ -78,15 +79,19 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ show, handleClose, /> </FormGroup> <FormGroup> - <Label for="mapDisplayable"><FormattedMessage id="map.displayable" /></Label> + <Label for='mapDisplayable'><FormattedMessage id='map.displayable' /></Label> <Input - id="mapDisplayable" - type="select" - value={displayable.toString()} - onChange={e => setDisplayable(e.target.value === 'true')} + id='mapDisplayable' + name='mapDisplayable' + type='select' + value={displayable} + onChange={e => setDisplayable(e.target.value as DisplayableType)} > - <option value="true">{intl.formatMessage({ id: 'map.is.displayable' })}</option> - <option value="false">{intl.formatMessage({ id: 'map.is.not.displayable' })}</option> + {Object.keys(DisplayableType).map(key => ( + <option value={key} key={key}> + {intl.formatMessage({ id: `map.is.${key}` })} + </option> + ))} </Input> </FormGroup> <FormGroup> diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 58a339921..48d338614 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -1299,7 +1299,7 @@ const LocaleTranslationData = { "map.calibration": "Estado de calibración: ", "map.circle.size": "Tamaño del círculo: ", "map.confirm.remove": "¿Estás seguro de que quieres eliminar el mapa", - "map.displayable": "Visuilización: ", + "map.displayable": "Visualización: ", "map.filename": "Archivo: ", "map.id": "ID del Mapa", "map.interval": "Intervalo de mapa", diff --git a/src/client/app/types/redux/map.ts b/src/client/app/types/redux/map.ts index d1700c035..d171dd272 100644 --- a/src/client/app/types/redux/map.ts +++ b/src/client/app/types/redux/map.ts @@ -14,6 +14,11 @@ export enum CalibrationModeTypes { unavailable = 'unavailable' } +export enum DisplayableType { + displayable = 'displayable', + notDisplayable = 'not.displayable' +} + export interface ChangeMapModeAction { type: ActionType.UpdateCalibrationMode; nextMode: CalibrationModeTypes; @@ -148,7 +153,7 @@ export interface MapData { export interface MapMetadata { id: number; name: string; - displayable: boolean; + displayable: DisplayableType; note?: string; filename: string; modifiedDate: string; From 4c5f66cb471aba9e03e9fa910f3e6e4f5ed69ce5 Mon Sep 17 00:00:00 2001 From: Juan Gutierrez <juanjoseguva@gmail.com> Date: Thu, 1 Aug 2024 11:23:00 -0700 Subject: [PATCH 27/50] Bug squashing --- src/client/app/components/maps/EditMapModalComponent.tsx | 6 +++--- src/client/app/components/maps/MapViewComponent.tsx | 3 +-- src/client/app/translations/data.ts | 8 ++++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/client/app/components/maps/EditMapModalComponent.tsx b/src/client/app/components/maps/EditMapModalComponent.tsx index 8eb543da7..d51974aad 100644 --- a/src/client/app/components/maps/EditMapModalComponent.tsx +++ b/src/client/app/components/maps/EditMapModalComponent.tsx @@ -79,10 +79,10 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ show, handleClose, /> </FormGroup> <FormGroup> - <Label for='mapDisplayable'><FormattedMessage id='map.displayable' /></Label> + <Label for='map.displayable'><FormattedMessage id='map.displayable' /></Label> <Input - id='mapDisplayable' - name='mapDisplayable' + id='map.displayable' + name='map.displayable' type='select' value={displayable} onChange={e => setDisplayable(e.target.value as DisplayableType)} diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index d6282ecf8..26090a602 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -7,7 +7,6 @@ import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Button } from 'reactstrap'; import { parseZone } from 'moment'; -import { hasToken } from '../../utils/token'; import '../../styles/card-page.css'; import EditMapModalComponent from './EditMapModalComponent'; import { makeSelectMapById } from '../../redux/selectors/maps'; @@ -54,7 +53,7 @@ const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { <FormattedMessage id={map.origin && map.opposite ? 'map.is.calibrated' : 'map.is.not.calibrated'} /> </span> </div> - {hasToken() && ( + {( <div className="edit-btn"> <Button color='secondary' onClick={handleShowModal}> <FormattedMessage id="edit.map" /> diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 48d338614..7c5170f37 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -313,7 +313,7 @@ const LocaleTranslationData = { "map.is.deleted": "Map removed from database", "map.is.displayable": "Display Enabled", "map.is.not.calibrated": "Calibration Needed", - "map.is.not.displayable": "Display Disabled", + "map.is.notDisplayable": "Display Disabled", "map.load.complete": "Map load complete from", "map.modified.date": "Last Modified: ", "map.name": "Map Name", @@ -808,9 +808,9 @@ const LocaleTranslationData = { "map.interval": "Intervalle de carte", "map.is.calibrated": "Étalonnage terminé", "map.is.deleted": "Carte supprimée de la base de données", - "map.is.displayable": "Affichage activé: ", + "map.is.displayable": "Affichage activé", "map.is.not.calibrated": "Étalonnage nécessaire", - "map.is.not.displayable": "Affichage désactivé", + "map.is.notDisplayable": "Affichage désactivé", "map.load.complete": "Chargement de la carte terminé à partir de", "map.modified.date": "Dernière modification: ", "map.name": "Nom de la carte", @@ -1307,7 +1307,7 @@ const LocaleTranslationData = { "map.is.deleted": "Mapa borrado de la base de datos", "map.is.displayable": "Visualización activada", "map.is.not.calibrated": "Necesita calibración", - "map.is.not.displayable": "Visualización desactivada", + "map.is.notDisplayable": "Visualización desactivada", "map.load.complete": "Carga de mapas completada de", "map.modified.date": "Última modificación: ", "map.name": "Nombre del mapa", From 09e0738cb4ad20b148bef82c6cf930e4d0c151fd Mon Sep 17 00:00:00 2001 From: Juan Gutierrez <juanjoseguva@gmail.com> Date: Thu, 1 Aug 2024 21:29:40 -0700 Subject: [PATCH 28/50] Aligning modal call function names with other components and limiting note length --- src/client/app/components/maps/MapViewComponent.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index 26090a602..26588afd2 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -17,8 +17,8 @@ interface MapViewProps { const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { const [showEditModal, setShowEditModal] = useState(false); - const handleShowModal = () => setShowEditModal(true); - const handleCloseModal = () => setShowEditModal(false); + const handleShow = () => setShowEditModal(true); + const handleClose = () => setShowEditModal(false); const selectMapById = React.useMemo(makeSelectMapById, []); const map = useSelector(state => selectMapById(state, mapID)); @@ -38,7 +38,7 @@ const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { <b><FormattedMessage id="map.circle.size" /></b> {map.circleSize} </div> <div className="item-container"> - <b><FormattedMessage id="note" /></b> {map.note} + <b><FormattedMessage id="note" /></b> {map.note ? map.note.slice(0,29) : ''} </div> <div className="item-container"> <b><FormattedMessage id="map.filename" /></b> {map.filename} @@ -55,14 +55,14 @@ const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { </div> {( <div className="edit-btn"> - <Button color='secondary' onClick={handleShowModal}> + <Button color='secondary' onClick={handleShow}> <FormattedMessage id="edit.map" /> </Button> </div> )} <EditMapModalComponent show={showEditModal} - handleClose={handleCloseModal} + handleClose={handleClose} map={map} /> </div> From 811dc6693629b4bef6f78de78981095a93f66e0d Mon Sep 17 00:00:00 2001 From: hazeltonbw <hazeltonbw@gmail.com> Date: Fri, 2 Aug 2024 22:37:12 -0700 Subject: [PATCH 29/50] Update code to use simple selector --- src/client/app/components/maps/MapViewComponent.tsx | 9 +++++---- src/client/app/redux/selectors/maps.ts | 9 ++------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index 26588afd2..27d15ac02 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -9,8 +9,10 @@ import { Button } from 'reactstrap'; import { parseZone } from 'moment'; import '../../styles/card-page.css'; import EditMapModalComponent from './EditMapModalComponent'; -import { makeSelectMapById } from '../../redux/selectors/maps'; -import { useSelector } from 'react-redux'; +import { selectMapById } from '../../redux/selectors/maps'; +import { RootState } from '../../store'; +import { useAppSelector } from '../../redux/reduxHooks'; +import { MapMetadata } from 'types/redux/map'; interface MapViewProps { mapID: number; } @@ -20,8 +22,7 @@ const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { const handleShow = () => setShowEditModal(true); const handleClose = () => setShowEditModal(false); - const selectMapById = React.useMemo(makeSelectMapById, []); - const map = useSelector(state => selectMapById(state, mapID)); + const map: MapMetadata = useAppSelector((state: RootState) => selectMapById(state, mapID)); return ( <div className="card"> diff --git a/src/client/app/redux/selectors/maps.ts b/src/client/app/redux/selectors/maps.ts index 909990754..bf4f67a38 100644 --- a/src/client/app/redux/selectors/maps.ts +++ b/src/client/app/redux/selectors/maps.ts @@ -4,6 +4,7 @@ import { RootState } from 'store'; import { createSelector } from '@reduxjs/toolkit'; +import { MapMetadata } from 'types/redux/map'; export const selectMapState = (state: RootState) => state.maps; export const selectMaps = createSelector([selectMapState], maps => { @@ -12,10 +13,4 @@ export const selectMaps = createSelector([selectMapState], maps => { .filter(key => !isNaN(key)); }); -export const makeSelectMapById = () => { - const selectMapById = createSelector( - [selectMapState, (state, id) => id], - (maps, id) => maps.byMapID[id] - ); - return selectMapById; -}; \ No newline at end of file +export const selectMapById = (state: RootState, id: number): MapMetadata => state.maps.byMapID[id]; \ No newline at end of file From 964122039ec094917fc4e17426e6eddff660f8b0 Mon Sep 17 00:00:00 2001 From: hazeltonbw <hazeltonbw@gmail.com> Date: Sat, 3 Aug 2024 03:35:54 -0700 Subject: [PATCH 30/50] Put modal state where it belongs --- .../components/maps/EditMapModalComponent.tsx | 178 +++++++++--------- .../app/components/maps/MapViewComponent.tsx | 14 -- 2 files changed, 93 insertions(+), 99 deletions(-) diff --git a/src/client/app/components/maps/EditMapModalComponent.tsx b/src/client/app/components/maps/EditMapModalComponent.tsx index d51974aad..2654f26ed 100644 --- a/src/client/app/components/maps/EditMapModalComponent.tsx +++ b/src/client/app/components/maps/EditMapModalComponent.tsx @@ -14,12 +14,13 @@ import { useAppDispatch } from '../../redux/reduxHooks'; import { AppDispatch } from 'store'; interface EditMapModalProps { - show: boolean; - handleClose: () => void; map: MapMetadata; } -const EditMapModalComponent: React.FC<EditMapModalProps> = ({ show, handleClose, map}) => { +const EditMapModalComponent: React.FC<EditMapModalProps> = ({ map }) => { + const [showModal, setShowModal] = useState(false); + const handleShow = () => setShowModal(true); + const handleClose = () => setShowModal(false); const dispatch: AppDispatch = useAppDispatch(); const [nameInput, setNameInput] = useState(map.name); const [noteInput, setNoteInput] = useState(map.note || ''); @@ -64,92 +65,99 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ show, handleClose, }; return ( - <Modal isOpen={show} toggle={handleClose}> - <ModalHeader toggle={handleClose}> - <FormattedMessage id="edit.map" /> - </ModalHeader> - <ModalBody> - <Form> - <FormGroup> - <Label for="mapName"><FormattedMessage id="map.name" /></Label> - <Input - id="mapName" - value={nameInput} - onChange={e => setNameInput(e.target.value)} - /> - </FormGroup> - <FormGroup> - <Label for='map.displayable'><FormattedMessage id='map.displayable' /></Label> + <> + <div className="edit-btn"> + <Button color='secondary' onClick={handleShow}> + <FormattedMessage id="edit.map" /> + </Button> + </div> + <Modal isOpen={showModal} toggle={handleClose}> + <ModalHeader toggle={handleClose}> + <FormattedMessage id="edit.map" /> + </ModalHeader> + <ModalBody> + <Form> + <FormGroup> + <Label for="mapName"><FormattedMessage id="map.name" /></Label> + <Input + id="mapName" + value={nameInput} + onChange={e => setNameInput(e.target.value)} + /> + </FormGroup> + <FormGroup> + <Label for='map.displayable'><FormattedMessage id='map.displayable' /></Label> + <Input + id='map.displayable' + name='map.displayable' + type='select' + value={displayable} + onChange={e => setDisplayable(e.target.value as DisplayableType)} + > + {Object.keys(DisplayableType).map(key => ( + <option value={key} key={key}> + {intl.formatMessage({ id: `map.is.${key}` })} + </option> + ))} + </Input> + </FormGroup> + <FormGroup> + <Label for="mapCircleSize"><FormattedMessage id="map.circle.size" /></Label> + <Input + id="mapCircleSize" + type='number' + value={circleInput} + onChange={e => setCircleInput(e.target.value)} + invalid={parseFloat(circleInput) < 0} + onBlur={toggleCircleEdit} + /> + </FormGroup> + <FormGroup> + <Label for="mapNote"><FormattedMessage id="note" /></Label> + <Input + id="mapNote" + type="textarea" + value={noteInput} + onChange={e => setNoteInput(e.target.value.slice(0, 30))} + /> + </FormGroup> + </Form> + <div> + <Label><FormattedMessage id="map.filename" /></Label> <Input - id='map.displayable' - name='map.displayable' - type='select' - value={displayable} - onChange={e => setDisplayable(e.target.value as DisplayableType)} - > - {Object.keys(DisplayableType).map(key => ( - <option value={key} key={key}> - {intl.formatMessage({ id: `map.is.${key}` })} - </option> - ))} + id='mapFilename' + name='mapFilename' + type='text' + defaultValue={map.filename} + disabled> </Input> - </FormGroup> - <FormGroup> - <Label for="mapCircleSize"><FormattedMessage id="map.circle.size" /></Label> - <Input - id="mapCircleSize" - type='number' - value={circleInput} - onChange={e => setCircleInput(e.target.value)} - invalid={parseFloat(circleInput)<0} - onBlur={toggleCircleEdit} - /> - </FormGroup> - <FormGroup> - <Label for="mapNote"><FormattedMessage id="note" /></Label> - <Input - id="mapNote" - type="textarea" - value={noteInput} - onChange={e => setNoteInput(e.target.value.slice(0,30))} - /> - </FormGroup> - </Form> - <div> - <Label><FormattedMessage id="map.filename" /></Label> - <Input - id='mapFilename' - name='mapFilename' - type='text' - defaultValue={map.filename} - disabled> - </Input> - <Button color='primary' onClick={() => handleCalibrationSetting(CalibrationModeTypes.initiate)}> - <FormattedMessage id='map.upload.new.file' /> + <Button color='primary' onClick={() => handleCalibrationSetting(CalibrationModeTypes.initiate)}> + <FormattedMessage id='map.upload.new.file' /> + </Button> + </div> + <div> + <Label><FormattedMessage id="map.calibration" /></Label> + <p> + <FormattedMessage id={map.origin && map.opposite ? 'map.is.calibrated' : 'map.is.not.calibrated'} /> + </p> + <Button color='primary' onClick={() => handleCalibrationSetting(CalibrationModeTypes.calibrate)}> + <FormattedMessage id='map.calibrate' /> + </Button> + </div> + </ModalBody> + <ModalFooter> + <Button color="danger" onClick={handleDelete}> + <FormattedMessage id="delete.map" /> </Button> - </div> - <div> - <Label><FormattedMessage id="map.calibration" /></Label> - <p> - <FormattedMessage id={map.origin && map.opposite ? 'map.is.calibrated' : 'map.is.not.calibrated'} /> - </p> - <Button color='primary' onClick={() => handleCalibrationSetting(CalibrationModeTypes.calibrate)}> - <FormattedMessage id='map.calibrate' /> + <Button color="secondary" onClick={handleClose}> + <FormattedMessage id="cancel" /> </Button> - </div> - </ModalBody> - <ModalFooter> - <Button color="danger" onClick={handleDelete}> - <FormattedMessage id="delete.map" /> - </Button> - <Button color="secondary" onClick={handleClose}> - <FormattedMessage id="cancel" /> - </Button> - <Button color="primary" onClick={handleSave}> - <FormattedMessage id="save.all" /> - </Button> - </ModalFooter> - </Modal> + <Button color="primary" onClick={handleSave}> + <FormattedMessage id="save.all" /> + </Button> + </ModalFooter> + </Modal> + </> ); }; diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index 27d15ac02..768df3b84 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -3,9 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Button } from 'reactstrap'; import { parseZone } from 'moment'; import '../../styles/card-page.css'; import EditMapModalComponent from './EditMapModalComponent'; @@ -18,9 +16,6 @@ interface MapViewProps { } const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { - const [showEditModal, setShowEditModal] = useState(false); - const handleShow = () => setShowEditModal(true); - const handleClose = () => setShowEditModal(false); const map: MapMetadata = useAppSelector((state: RootState) => selectMapById(state, mapID)); @@ -54,16 +49,7 @@ const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { <FormattedMessage id={map.origin && map.opposite ? 'map.is.calibrated' : 'map.is.not.calibrated'} /> </span> </div> - {( - <div className="edit-btn"> - <Button color='secondary' onClick={handleShow}> - <FormattedMessage id="edit.map" /> - </Button> - </div> - )} <EditMapModalComponent - show={showEditModal} - handleClose={handleClose} map={map} /> </div> From 1e0b61b3bfa887c829d8816355c303788d890d6f Mon Sep 17 00:00:00 2001 From: Juan Gutierrez <juanjoseguva@gmail.com> Date: Sat, 3 Aug 2024 09:34:04 -0700 Subject: [PATCH 31/50] Reverting to treating map displayability variable as a boolean instead of enum --- .../components/maps/EditMapModalComponent.tsx | 21 +++++++------------ src/client/app/translations/data.ts | 2 -- src/client/app/types/redux/map.ts | 7 +------ 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/src/client/app/components/maps/EditMapModalComponent.tsx b/src/client/app/components/maps/EditMapModalComponent.tsx index 2654f26ed..fba8a82a0 100644 --- a/src/client/app/components/maps/EditMapModalComponent.tsx +++ b/src/client/app/components/maps/EditMapModalComponent.tsx @@ -8,7 +8,6 @@ import { FormattedMessage, useIntl } from 'react-intl'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input } from 'reactstrap'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; import { editMapDetails, submitEditedMap, removeMap, setCalibration } from '../../redux/actions/map'; -import { DisplayableType } from '../../types/redux/map'; import { showErrorNotification } from '../../utils/notifications'; import { useAppDispatch } from '../../redux/reduxHooks'; import { AppDispatch } from 'store'; @@ -25,7 +24,7 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ map }) => { const [nameInput, setNameInput] = useState(map.name); const [noteInput, setNoteInput] = useState(map.note || ''); const [circleInput, setCircleInput] = useState(map.circleSize.toString()); - const [displayable, setDisplayable] = useState<DisplayableType>(map.displayable); + const [displayable, setDisplayable] = useState(map.displayable); const intl = useIntl(); @@ -35,7 +34,7 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ map }) => { name: nameInput, note: noteInput, circleSize: parseFloat(circleInput), - displayable: displayable as DisplayableType + displayable: displayable }; dispatch(editMapDetails(updatedMap)); dispatch(submitEditedMap(updatedMap.id)); @@ -88,17 +87,13 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ map }) => { <FormGroup> <Label for='map.displayable'><FormattedMessage id='map.displayable' /></Label> <Input - id='map.displayable' - name='map.displayable' - type='select' - value={displayable} - onChange={e => setDisplayable(e.target.value as DisplayableType)} + id="mapDisplayable" + type="select" + value={displayable.toString()} + onChange={e => setDisplayable(e.target.value === 'true')} > - {Object.keys(DisplayableType).map(key => ( - <option value={key} key={key}> - {intl.formatMessage({ id: `map.is.${key}` })} - </option> - ))} + <option value="true">{intl.formatMessage({ id: 'map.is.displayable' })}</option> + <option value="false">{intl.formatMessage({ id: 'map.is.not.displayable' })}</option> </Input> </FormGroup> <FormGroup> diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index c77041f84..d4d1aaf2e 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -825,7 +825,6 @@ const LocaleTranslationData = { "map.notify.calibration.needed": "Étalonnage nécessaire pour l'affichage", "map.upload.new.file": "Upload New File\u{26A1}", "map.unavailable": "There's not an available map\u{26A1}", - "map.upload.new.file": "Refaire", "max": "max\u{26A1}", "menu": "Menu", "meter": "Mèter", @@ -1327,7 +1326,6 @@ const LocaleTranslationData = { "map.notify.calibration.needed": "Necesita calibración antes de visualizar", "map.upload.new.file": "Subir un nuevo archivo", "map.unavailable": "No hay un mapa disponible", - "map.upload.new.file": "Rehacer", "max": "máximo", "menu": "Menú", "meter": "Medidor", diff --git a/src/client/app/types/redux/map.ts b/src/client/app/types/redux/map.ts index d171dd272..d1700c035 100644 --- a/src/client/app/types/redux/map.ts +++ b/src/client/app/types/redux/map.ts @@ -14,11 +14,6 @@ export enum CalibrationModeTypes { unavailable = 'unavailable' } -export enum DisplayableType { - displayable = 'displayable', - notDisplayable = 'not.displayable' -} - export interface ChangeMapModeAction { type: ActionType.UpdateCalibrationMode; nextMode: CalibrationModeTypes; @@ -153,7 +148,7 @@ export interface MapData { export interface MapMetadata { id: number; name: string; - displayable: DisplayableType; + displayable: boolean; note?: string; filename: string; modifiedDate: string; From 51a220f23a762ee9ed372cce910ea7915b292766 Mon Sep 17 00:00:00 2001 From: Severin L <severinlight3@gmail.com> Date: Sat, 3 Aug 2024 18:46:57 +0000 Subject: [PATCH 32/50] Add coloring to map Display and add TODO for date internationalization --- .../app/components/maps/MapViewComponent.tsx | 26 +++++++++++++------ src/client/app/translations/data.ts | 20 +++++++------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index 768df3b84..5211edb1b 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -11,6 +11,8 @@ import { selectMapById } from '../../redux/selectors/maps'; import { RootState } from '../../store'; import { useAppSelector } from '../../redux/reduxHooks'; import { MapMetadata } from 'types/redux/map'; +import translate from '../../utils/translate'; +import { LocaleDataKey } from 'translations/data'; interface MapViewProps { mapID: number; } @@ -19,34 +21,42 @@ const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { const map: MapMetadata = useAppSelector((state: RootState) => selectMapById(state, mapID)); + // Helper function checks map to see if it's calibrated + const getCalibrationStatus = () => { + const isCalibrated = map.origin && map.opposite; + return { + color: isCalibrated ? 'black' : 'gray', + messageId: isCalibrated ? 'map.is.calibrated' : 'map.is.not.calibrated' + }; + }; + const { color, messageId } = getCalibrationStatus(); + return ( <div className="card"> <div className="identifier-container"> {map.name} </div> - <div className="item-container"> - <b><FormattedMessage id="map.displayable" /></b> - <span style={{ color: map.displayable ? 'green' : 'red' }}> - <FormattedMessage id={map.displayable ? 'map.is.displayable' : 'map.is.not.displayable'} /> - </span> + <div className={map.displayable.toString()}> + <b><FormattedMessage id="map.displayable" /></b> {translate(`TrueFalseType.${map.displayable.toString()}` as LocaleDataKey)} </div> <div className="item-container"> <b><FormattedMessage id="map.circle.size" /></b> {map.circleSize} </div> <div className="item-container"> - <b><FormattedMessage id="note" /></b> {map.note ? map.note.slice(0,29) : ''} + <b><FormattedMessage id="note" /></b> {map.note ? map.note.slice(0, 29) : ''} </div> <div className="item-container"> <b><FormattedMessage id="map.filename" /></b> {map.filename} </div> <div className="item-container"> <b><FormattedMessage id="map.modified.date" /></b> + {/* TODO I don't think this will properly internationalize. */} {parseZone(map.modifiedDate, undefined, true).format('dddd, MMM DD, YYYY hh:mm a')} </div> <div className="item-container"> <b><FormattedMessage id="map.calibration" /></b> - <span style={{ color: map.origin && map.opposite ? 'black' : 'gray' }}> - <FormattedMessage id={map.origin && map.opposite ? 'map.is.calibrated' : 'map.is.not.calibrated'} /> + <span style={{ color }}> + <FormattedMessage id={messageId} /> </span> </div> <EditMapModalComponent diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index d4d1aaf2e..deb2c9fc7 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -151,7 +151,7 @@ const LocaleTranslationData = { "DisplayableType.none": "none", "DisplayableType.all": "all", "DisplayableType.admin": "admin", - "done.editing":"Done editing", + "done.editing": "Done editing", "error.bounds": "Must be between {min} and {max}.", "error.displayable": "Displayable will be set to false because no unit is selected.", "error.displayable.meter": "Meter units will set displayable to none.", @@ -166,7 +166,7 @@ const LocaleTranslationData = { "edit.a.group": "Edit a Group", "edit.a.meter": "Edit a Meter", "edit.group": "Edit Group", - "edit.map":"Edit Map", + "edit.map": "Edit Map", "edit.meter": "Details/Edit Meter", "edit.unit": "Edit Unit", "email": "Email", @@ -311,9 +311,9 @@ const LocaleTranslationData = { "map.interval": "Map Interval", "map.is.calibrated": "Calibration Complete", "map.is.deleted": "Map removed from database", - "map.is.displayable": "Display Enabled", + "map.is.displayable": "true", "map.is.not.calibrated": "Calibration Needed", - "map.is.notDisplayable": "Display Disabled", + "map.is.not.displayable": "false", "map.load.complete": "Map load complete from", "map.modified.date": "Last Modified: ", "map.name": "Map Name", @@ -652,7 +652,7 @@ const LocaleTranslationData = { "DisplayableType.none": "none\u{26A1}", "DisplayableType.all": "all\u{26A1}", "DisplayableType.admin": "admin\u{26A1}", - "done.editing":"Done editing\u{26A1}", + "done.editing": "Done editing\u{26A1}", "error.bounds": "Must be between {min} and {max}.\u{26A1}", "error.displayable": "Displayable will be set to false because no unit is selected.\u{26A1}", "error.displayable.meter": "Meter units will set displayable to none.\u{26A1}", @@ -667,7 +667,7 @@ const LocaleTranslationData = { "edit.a.group": "Modifier le Groupe", "edit.a.meter": "Modifier le Métre", "edit.group": "Modifier Groupe", - "edit.map":"Edit Map\u{26A1}", + "edit.map": "Edit Map\u{26A1}", "edit.meter": "Details/Modifier Métre\u{26A1}", "edit.unit": "Edit Unit\u{26A1}", "email": "E-mail", @@ -1153,7 +1153,7 @@ const LocaleTranslationData = { "DisplayableType.none": "ninguno", "DisplayableType.all": "todo", "DisplayableType.admin": "administrador", - "done.editing":"Acabar de editar", + "done.editing": "Acabar de editar", "error.bounds": "Debe ser entre {min} y {max}.", "error.displayable": "El elemento visual determinado como falso porque no hay unidad seleccionada.", "error.displayable.meter": "Las unidades de medición determinarán al elemento visual como ninguno.", @@ -1162,13 +1162,13 @@ const LocaleTranslationData = { "error.gps": "Latitud deber ser entre -90 y 90, y longitud de entre -180 y 180.", "error.negative": "No puede ser negativo.", "error.required": "Campo requerido", - "error.unknown":"¡Ups! Ha ocurrido un error.", + "error.unknown": "¡Ups! Ha ocurrido un error.", "edit": "Editar", "edited": "editado", "edit.a.group": "Editar un grupo", "edit.a.meter": "Editar un medidor", "edit.group": "Editar grupo", - "edit.map":"Editar mapa", + "edit.map": "Editar mapa", "edit.meter": "Editar medidor", "edit.unit": "Editar unidad", "email": "Correo electrónico", @@ -1499,7 +1499,7 @@ const LocaleTranslationData = { "users.successfully.create.user": "Usuario creado con éxito.", "users.successfully.delete.user": "Usuario borrado con éxito.", "users.successfully.edit.users": "Usuarios editados con éxito.", - "uses":"usos", + "uses": "usos", "view.groups": "Ver grupos", "visit": " o visite nuestro ", "website": "sitio web", From d270242dbf65ef6dce23cefc65ce95fb3610b163 Mon Sep 17 00:00:00 2001 From: hazeltonbw <hazeltonbw@gmail.com> Date: Sat, 3 Aug 2024 20:11:56 -0700 Subject: [PATCH 33/50] Use memoized createAppSelector instead of directly handling state in selectors. --- .../app/components/maps/MapViewComponent.tsx | 3 +-- src/client/app/redux/selectors/maps.ts | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index 5211edb1b..a2bd3a276 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -8,7 +8,6 @@ import { parseZone } from 'moment'; import '../../styles/card-page.css'; import EditMapModalComponent from './EditMapModalComponent'; import { selectMapById } from '../../redux/selectors/maps'; -import { RootState } from '../../store'; import { useAppSelector } from '../../redux/reduxHooks'; import { MapMetadata } from 'types/redux/map'; import translate from '../../utils/translate'; @@ -19,7 +18,7 @@ interface MapViewProps { const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { - const map: MapMetadata = useAppSelector((state: RootState) => selectMapById(state, mapID)); + const map: MapMetadata = useAppSelector(selectMapById(mapID)); // Helper function checks map to see if it's calibrated const getCalibrationStatus = () => { diff --git a/src/client/app/redux/selectors/maps.ts b/src/client/app/redux/selectors/maps.ts index bf4f67a38..020fa7275 100644 --- a/src/client/app/redux/selectors/maps.ts +++ b/src/client/app/redux/selectors/maps.ts @@ -2,15 +2,16 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { RootState } from 'store'; -import { createSelector } from '@reduxjs/toolkit'; -import { MapMetadata } from 'types/redux/map'; +import { RootState } from "store"; +import { MapState } from "types/redux/map"; +import { createAppSelector } from "./selectors"; export const selectMapState = (state: RootState) => state.maps; -export const selectMaps = createSelector([selectMapState], maps => { - return Object.keys(maps.byMapID) - .map(key => parseInt(key)) - .filter(key => !isNaN(key)); -}); +export const selectMaps = createAppSelector([selectMapState], (maps) => + Object.keys(maps.byMapID) + .map((key) => parseInt(key)) + .filter((key) => !isNaN(key)) +); -export const selectMapById = (state: RootState, id: number): MapMetadata => state.maps.byMapID[id]; \ No newline at end of file +export const selectMapById = (id: number) => + createAppSelector([selectMapState], (maps: MapState) => maps.byMapID[id]); From 18fa27558489e3bcb1a8526480e7c7fba98d67ed Mon Sep 17 00:00:00 2001 From: hazeltonbw <hazeltonbw@gmail.com> Date: Sat, 3 Aug 2024 20:17:08 -0700 Subject: [PATCH 34/50] Fix linting errors --- src/client/app/redux/selectors/maps.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/app/redux/selectors/maps.ts b/src/client/app/redux/selectors/maps.ts index 020fa7275..f0fd34ce1 100644 --- a/src/client/app/redux/selectors/maps.ts +++ b/src/client/app/redux/selectors/maps.ts @@ -2,15 +2,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { RootState } from "store"; -import { MapState } from "types/redux/map"; -import { createAppSelector } from "./selectors"; +import { RootState } from 'store'; +import { MapState } from 'types/redux/map'; +import { createAppSelector } from './selectors'; export const selectMapState = (state: RootState) => state.maps; -export const selectMaps = createAppSelector([selectMapState], (maps) => +export const selectMaps = createAppSelector([selectMapState], maps => Object.keys(maps.byMapID) - .map((key) => parseInt(key)) - .filter((key) => !isNaN(key)) + .map(key => parseInt(key)) + .filter(key => !isNaN(key)) ); export const selectMapById = (id: number) => From ca0860660f15275e2a40771fa99cb0471d4d2403 Mon Sep 17 00:00:00 2001 From: hazeltonbw <hazeltonbw@gmail.com> Date: Sun, 4 Aug 2024 18:33:05 -0700 Subject: [PATCH 35/50] Leave TODO notes for future updates --- src/client/app/components/maps/CreateMapModalComponent.tsx | 4 +++- src/client/app/components/maps/EditMapModalComponent.tsx | 3 ++- src/client/app/components/maps/MapViewComponent.tsx | 3 ++- src/client/app/components/maps/MapsDetailComponent.tsx | 4 +++- src/client/app/redux/actions/map.ts | 4 +++- src/client/app/redux/reducers/maps.ts | 4 +++- src/client/app/redux/selectors/maps.ts | 2 ++ 7 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/client/app/components/maps/CreateMapModalComponent.tsx b/src/client/app/components/maps/CreateMapModalComponent.tsx index 213113abf..607e71d24 100644 --- a/src/client/app/components/maps/CreateMapModalComponent.tsx +++ b/src/client/app/components/maps/CreateMapModalComponent.tsx @@ -23,6 +23,8 @@ interface CreateMapModalProps { * @returns Map create element */ function CreateMapModalComponent({ show, handleClose, createNewMap }: CreateMapModalProps) { + // TODO: Get rid of props, migrate to RTK, finish modal + // Once modal is finished, it will be used in MapsDetailComponent const [nameInput, setNameInput] = useState(''); const [noteInput, setNoteInput] = useState(''); @@ -77,4 +79,4 @@ function CreateMapModalComponent({ show, handleClose, createNewMap }: CreateMapM ); } -export default CreateMapModalComponent; \ No newline at end of file +export default CreateMapModalComponent; diff --git a/src/client/app/components/maps/EditMapModalComponent.tsx b/src/client/app/components/maps/EditMapModalComponent.tsx index fba8a82a0..588f4bfd3 100644 --- a/src/client/app/components/maps/EditMapModalComponent.tsx +++ b/src/client/app/components/maps/EditMapModalComponent.tsx @@ -16,6 +16,7 @@ interface EditMapModalProps { map: MapMetadata; } +// TODO: Migrate to RTK const EditMapModalComponent: React.FC<EditMapModalProps> = ({ map }) => { const [showModal, setShowModal] = useState(false); const handleShow = () => setShowModal(true); @@ -156,4 +157,4 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ map }) => { ); }; -export default EditMapModalComponent; \ No newline at end of file +export default EditMapModalComponent; diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index a2bd3a276..4156ef9bc 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -16,6 +16,7 @@ interface MapViewProps { mapID: number; } +//TODO: Migrate to RTK const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { const map: MapMetadata = useAppSelector(selectMapById(mapID)); @@ -65,4 +66,4 @@ const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { ); }; -export default MapViewComponent; \ No newline at end of file +export default MapViewComponent; diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index 957f20791..0f7be9289 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -19,6 +19,7 @@ import { AppDispatch } from 'store'; * Defines the maps page card view * @returns Maps page element */ +// TODO: Migrate to RTK export default function MapsDetailComponent() { const dispatch: AppDispatch = useAppDispatch(); // Load map IDs from state and store in number array @@ -38,6 +39,7 @@ export default function MapsDetailComponent() { <TooltipMarkerComponent page='maps' helpTextId='help.admin.mapview' /> </div> </h2> + { /* TODO: Change Link to <CreateMapModalComponent /> when it is completed */ } <div className="edit-btn"> <Link to='/calibration' onClick={() => dispatch(setNewMap())}> <Button color='primary'> @@ -53,4 +55,4 @@ export default function MapsDetailComponent() { </div> </div> ); -} \ No newline at end of file +} diff --git a/src/client/app/redux/actions/map.ts b/src/client/app/redux/actions/map.ts index 27bc4b47a..5af42df04 100644 --- a/src/client/app/redux/actions/map.ts +++ b/src/client/app/redux/actions/map.ts @@ -2,6 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// TODO: Migrate to RTK + import { ActionType, Dispatch, GetState, Thunk } from '../../types/redux/actions'; import * as t from '../../types/redux/map'; import { CalibrationModeTypes, MapData, MapMetadata } from '../../types/redux/map'; @@ -335,4 +337,4 @@ export function confirmEditedMaps() { dispatch(confirmMapEdits(mapID)); }); }; -} \ No newline at end of file +} diff --git a/src/client/app/redux/reducers/maps.ts b/src/client/app/redux/reducers/maps.ts index 714980d5d..b43a0e789 100644 --- a/src/client/app/redux/reducers/maps.ts +++ b/src/client/app/redux/reducers/maps.ts @@ -2,6 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// TODO: Migrate to RTK + import { MapMetadata, MapsAction, MapState } from '../../types/redux/map'; import { ActionType } from '../../types/redux/actions'; import { keyBy } from 'lodash'; @@ -240,4 +242,4 @@ export default function maps(state = defaultState, action: MapsAction) { default: return state; } -} \ No newline at end of file +} diff --git a/src/client/app/redux/selectors/maps.ts b/src/client/app/redux/selectors/maps.ts index f0fd34ce1..fe068ee73 100644 --- a/src/client/app/redux/selectors/maps.ts +++ b/src/client/app/redux/selectors/maps.ts @@ -2,6 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// TODO: Migrate to RTK + import { RootState } from 'store'; import { MapState } from 'types/redux/map'; import { createAppSelector } from './selectors'; From 8a276b4ea7856c669e49326dcb4a35b099d7a76c Mon Sep 17 00:00:00 2001 From: Steven Huss-Lederman <huss@beloit.edu> Date: Mon, 5 Aug 2024 10:44:26 -0500 Subject: [PATCH 36/50] fix linting --- src/client/app/components/maps/CreateMapModalComponent.tsx | 6 ++---- src/client/app/components/maps/MapsDetailComponent.tsx | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/client/app/components/maps/CreateMapModalComponent.tsx b/src/client/app/components/maps/CreateMapModalComponent.tsx index 607e71d24..7980a82aa 100644 --- a/src/client/app/components/maps/CreateMapModalComponent.tsx +++ b/src/client/app/components/maps/CreateMapModalComponent.tsx @@ -16,12 +16,10 @@ interface CreateMapModalProps { /** * Defines the create map modal form - * @param root0 - * @param root0.show - * @param root0.handleClose - * @param root0.createNewMap + * params not given since props should be going away and painful. Remove eslint command when fixed. * @returns Map create element */ +/* eslint-disable-next-line */ function CreateMapModalComponent({ show, handleClose, createNewMap }: CreateMapModalProps) { // TODO: Get rid of props, migrate to RTK, finish modal // Once modal is finished, it will be used in MapsDetailComponent diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index 0f7be9289..5d6d8183c 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -10,7 +10,7 @@ import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import MapViewComponent from './MapViewComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import '../../styles/card-page.css'; -import { fetchMapsDetails, setNewMap} from '../../redux/actions/map'; +import { fetchMapsDetails, setNewMap } from '../../redux/actions/map'; import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; import { selectMaps } from '../../redux/selectors/maps'; import { AppDispatch } from 'store'; @@ -39,7 +39,7 @@ export default function MapsDetailComponent() { <TooltipMarkerComponent page='maps' helpTextId='help.admin.mapview' /> </div> </h2> - { /* TODO: Change Link to <CreateMapModalComponent /> when it is completed */ } + { /* TODO: Change Link to <CreateMapModalComponent /> when it is completed */} <div className="edit-btn"> <Link to='/calibration' onClick={() => dispatch(setNewMap())}> <Button color='primary'> From 21de91613eef696c0896d6de0eb3d8c27044e0ac Mon Sep 17 00:00:00 2001 From: ChrisMart21 <ChrisMart21@github.com> Date: Mon, 5 Aug 2024 12:09:07 -0700 Subject: [PATCH 37/50] Initial RTKQ Maps --- .../app/components/MapChartComponent.tsx | 499 +++++++++--------- .../maps/MapCalibrationInitiateComponent.tsx | 276 +++++----- .../app/components/maps/MapViewComponent.tsx | 13 +- .../components/maps/MapsDetailComponent.tsx | 19 +- .../app/containers/MapChartContainer.ts | 8 +- .../MapCalibrationChartDisplayContainer.ts | 6 +- src/client/app/redux/actions/map.ts | 18 +- src/client/app/redux/api/baseApi.ts | 1 + src/client/app/redux/api/logApi.ts | 14 + src/client/app/redux/api/mapsApi.ts | 147 ++++++ src/client/app/redux/devToolConfig.ts | 21 + src/client/app/redux/reducers/maps.ts | 3 +- src/client/app/redux/selectors/uiSelectors.ts | 5 +- src/client/app/redux/slices/appStateSlice.ts | 5 +- src/client/app/store.ts | 14 +- src/client/app/types/redux/map.ts | 12 +- 16 files changed, 616 insertions(+), 445 deletions(-) create mode 100644 src/client/app/redux/api/logApi.ts create mode 100644 src/client/app/redux/api/mapsApi.ts create mode 100644 src/client/app/redux/devToolConfig.ts diff --git a/src/client/app/components/MapChartComponent.tsx b/src/client/app/components/MapChartComponent.tsx index e1a5dde28..7ce9d34b8 100644 --- a/src/client/app/components/MapChartComponent.tsx +++ b/src/client/app/components/MapChartComponent.tsx @@ -5,8 +5,6 @@ import { orderBy } from 'lodash'; import * as moment from 'moment'; import * as React from 'react'; -import Plot from 'react-plotly.js'; -import { useSelector } from 'react-redux'; import { selectAreaUnit, selectBarWidthDays, selectGraphAreaNormalization, selectSelectedGroups, @@ -19,7 +17,6 @@ import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppSelector } from '../redux/reduxHooks'; import { selectMapChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { DataType } from '../types/Datasources'; -import { State } from '../types/redux/state'; import { UnitRepresentType } from '../types/redux/units'; import { CartesianPoint, @@ -34,6 +31,8 @@ import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConvers import getGraphColor from '../utils/getGraphColor'; import translate from '../utils/translate'; import SpinnerComponent from './SpinnerComponent'; +import { selectMapById } from '../redux/api/mapsApi'; +import Plot from 'react-plotly.js'; /** * @returns map component @@ -57,285 +56,271 @@ export default function MapChartComponent() { // RTK Types Disagree with maps ts types so, use old until migration completer for maps. // This is also an issue when trying to refactor maps reducer into slice. - const selectedMap = useSelector((state: State) => state.maps.selectedMap); - const byMapID = useSelector((state: State) => state.maps.byMapID); - const editedMaps = useSelector((state: State) => state.maps.editedMaps); + const selectedMap = useAppSelector(state => state.maps.selectedMap); + const map = useAppSelector(state => selectMapById(state, selectedMap)); + if (meterIsFetching || groupIsFetching) { return <SpinnerComponent loading width={50} height={50} />; } - - // Map to use. - let map; // Holds Plotly mapping info. const data = []; - // Holds the image to use. - let image; - if (selectedMap !== 0) { - const mapID = selectedMap; - if (byMapID[mapID]) { - map = byMapID[mapID]; - if (editedMaps[mapID]) { - map = editedMaps[mapID]; + + // Holds the hover text for each point for Plotly + const hoverText: string[] = []; + // Holds the size of each circle for Plotly. + const size: number[] = []; + // Holds the color of each circle for Plotly. + const colors: string[] = []; + // If there is no map then use a new, empty image as the map. I believe this avoids errors + // and gives the blank screen. + // Arrays to hold the Plotly grid location (x, y) for circles to place on map. + const x: number[] = []; + const y: number[] = []; + + // const timeInterval = state.graph.queryTimeInterval; + // const barDuration = state.graph.barDuration + // Make sure there is a map with values so avoid issues. + if (map && map.origin && map.opposite) { + // The size of the original map loaded into OED. + const imageDimensions: Dimensions = { + width: map.imgWidth, + height: map.imgHeight + }; + // Determine the dimensions so within the Plotly coordinates on the user map. + const imageDimensionNormalized = normalizeImageDimensions(imageDimensions); + // This is the origin & opposite from the calibration. It is the lower, left + // and upper, right corners of the user map. + const origin = map.origin; + const opposite = map.opposite; + // Get the GPS degrees per unit of Plotly grid for x and y. By knowing the two corners + // (or really any two distinct points) you can calculate this by the change in GPS over the + // change in x or y which is the map's width & height in this case. + const scaleOfMap = calculateScaleFromEndpoints(origin, opposite, imageDimensionNormalized, map.northAngle); + // Loop over all selected meters. Maps only work for meters at this time. + // The y-axis label depends on the unit which is in selectUnit state. + let unitLabel: string = ''; + // If graphingUnit is -99 then none selected and nothing to graph so label is empty. + // This will probably happen when the page is first loaded. + if (unitID !== -99) { + const selectUnitState = unitDataById[unitID]; + if (selectUnitState !== undefined) { + // Quantity and flow units have different unit labels. + // Look up the type of unit if it is for quantity/flow (should not be raw) and decide what to do. + // Bar graphics are always quantities. + if (selectUnitState.unitRepresent === UnitRepresentType.quantity) { + // If it is a quantity unit then that is the unit you are graphing but it is normalized to per day. + unitLabel = selectUnitState.identifier + ' / day'; + } else if (selectUnitState.unitRepresent === UnitRepresentType.flow) { + // If it is a flow meter then you need to multiply by time to get the quantity unit then show as per day. + // The quantity/time for flow has varying time so label by multiplying by time. + // To make sure it is clear, also indicate it is a quantity. + // Note this should not be used for raw data. + // It might not be usual to take a flow and make it into a quantity so this label is a little different to + // catch people's attention. If sites/users don't like OED doing this then we can eliminate flow for these types + // of graphics as we are doing for rate. + unitLabel = selectUnitState.identifier + ' * time / day ≡ quantity / day'; + } + if (areaNormalization) { + unitLabel += ' / ' + translate(`AreaUnitType.${selectedAreaUnit}`); + } } } - // Holds the hover text for each point for Plotly - const hoverText: string[] = []; - // Holds the size of each circle for Plotly. - const size: number[] = []; - // Holds the color of each circle for Plotly. - const colors: string[] = []; - // If there is no map then use a new, empty image as the map. I believe this avoids errors - // and gives the blank screen. - image = (map) ? map.image : new Image(); - // Arrays to hold the Plotly grid location (x, y) for circles to place on map. - const x: number[] = []; - const y: number[] = []; - // const timeInterval = state.graph.queryTimeInterval; - // const barDuration = state.graph.barDuration - // Make sure there is a map with values so avoid issues. - if (map && map.origin && map.opposite) { - // The size of the original map loaded into OED. - const imageDimensions: Dimensions = { - width: image.width, - height: image.height - }; - // Determine the dimensions so within the Plotly coordinates on the user map. - const imageDimensionNormalized = normalizeImageDimensions(imageDimensions); - // This is the origin & opposite from the calibration. It is the lower, left - // and upper, right corners of the user map. - const origin = map.origin; - const opposite = map.opposite; - // Get the GPS degrees per unit of Plotly grid for x and y. By knowing the two corners - // (or really any two distinct points) you can calculate this by the change in GPS over the - // change in x or y which is the map's width & height in this case. - const scaleOfMap = calculateScaleFromEndpoints(origin, opposite, imageDimensionNormalized, map.northAngle); - // Loop over all selected meters. Maps only work for meters at this time. - // The y-axis label depends on the unit which is in selectUnit state. - let unitLabel: string = ''; - // If graphingUnit is -99 then none selected and nothing to graph so label is empty. - // This will probably happen when the page is first loaded. - if (unitID !== -99) { - const selectUnitState = unitDataById[unitID]; - if (selectUnitState !== undefined) { - // Quantity and flow units have different unit labels. - // Look up the type of unit if it is for quantity/flow (should not be raw) and decide what to do. - // Bar graphics are always quantities. - if (selectUnitState.unitRepresent === UnitRepresentType.quantity) { - // If it is a quantity unit then that is the unit you are graphing but it is normalized to per day. - unitLabel = selectUnitState.identifier + ' / day'; - } else if (selectUnitState.unitRepresent === UnitRepresentType.flow) { - // If it is a flow meter then you need to multiply by time to get the quantity unit then show as per day. - // The quantity/time for flow has varying time so label by multiplying by time. - // To make sure it is clear, also indicate it is a quantity. - // Note this should not be used for raw data. - // It might not be usual to take a flow and make it into a quantity so this label is a little different to - // catch people's attention. If sites/users don't like OED doing this then we can eliminate flow for these types - // of graphics as we are doing for rate. - unitLabel = selectUnitState.identifier + ' * time / day ≡ quantity / day'; - } + for (const meterID of selectedMeters) { + // Get meter id number. + // Get meter GPS value. + const gps = meterDataById[meterID].gps; + // filter meters with actual gps coordinates. + if (gps !== undefined && gps !== null && meterReadings !== undefined) { + let meterArea = meterDataById[meterID].area; + // we either don't care about area, or we do in which case there needs to be a nonzero area + if (!areaNormalization || (meterArea > 0 && meterDataById[meterID].areaUnit != AreaUnitType.none)) { if (areaNormalization) { - unitLabel += ' / ' + translate(`AreaUnitType.${selectedAreaUnit}`); + // convert the meter area into the proper unit, if needed + meterArea *= getAreaUnitConversion(meterDataById[meterID].areaUnit, selectedAreaUnit); } - } - } - - for (const meterID of selectedMeters) { - // Get meter id number. - // Get meter GPS value. - const gps = meterDataById[meterID].gps; - // filter meters with actual gps coordinates. - if (gps !== undefined && gps !== null && meterReadings !== undefined) { - let meterArea = meterDataById[meterID].area; - // we either don't care about area, or we do in which case there needs to be a nonzero area - if (!areaNormalization || (meterArea > 0 && meterDataById[meterID].areaUnit != AreaUnitType.none)) { - if (areaNormalization) { - // convert the meter area into the proper unit, if needed - meterArea *= getAreaUnitConversion(meterDataById[meterID].areaUnit, selectedAreaUnit); - } - // Convert the gps value to the equivalent Plotly grid coordinates on user map. - // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. - // It must be on true north map since only there are the GPS axis parallel to the map axis. - // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating - // it coordinates on the true north map and then rotating/shifting to the user map. - const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); - // Only display items within valid info and within map. - if (itemMapInfoOk(meterID, DataType.Meter, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid)) { - // The x, y value for Plotly to use that are on the user map. - x.push(meterGPSInUserGrid.x); - y.push(meterGPSInUserGrid.y); - // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed - // and be fetching. The unit could change from that menu so also need to check. - // Get the bar data to use for the map circle. - // const readingsData = meterReadings[timeInterval.toString()][barDuration.toISOString()][unitID]; - const readingsData = meterReadings[meterID]; - // This protects against there being no readings or that the data is being updated. - if (readingsData !== undefined && !meterIsFetching) { - // Meter name to include in hover on graph. - const label = meterDataById[meterID].identifier; - // The usual color for this meter. - colors.push(getGraphColor(meterID, DataType.Meter)); - if (!readingsData) { - throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + // Convert the gps value to the equivalent Plotly grid coordinates on user map. + // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. + // It must be on true north map since only there are the GPS axis parallel to the map axis. + // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating + // it coordinates on the true north map and then rotating/shifting to the user map. + const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); + // Only display items within valid info and within map. + if (itemMapInfoOk(meterID, DataType.Meter, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid)) { + // The x, y value for Plotly to use that are on the user map. + x.push(meterGPSInUserGrid.x); + y.push(meterGPSInUserGrid.y); + // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed + // and be fetching. The unit could change from that menu so also need to check. + // Get the bar data to use for the map circle. + // const readingsData = meterReadings[timeInterval.toString()][barDuration.toISOString()][unitID]; + const readingsData = meterReadings[meterID]; + // This protects against there being no readings or that the data is being updated. + if (readingsData !== undefined && !meterIsFetching) { + // Meter name to include in hover on graph. + const label = meterDataById[meterID].identifier; + // The usual color for this meter. + colors.push(getGraphColor(meterID, DataType.Meter)); + if (!readingsData) { + throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + } + // Use the most recent time reading for the circle on the map. + // This has the limitations of the bar value where the last one can include ranges without + // data (GitHub issue on this). + // TODO: It might be better to do this similarly to compare. (See GitHub issue) + const readings = orderBy(readingsData, ['startTimestamp'], ['desc']); + const mapReading = readings[0]; + let timeReading: string; + let averagedReading = 0; + if (readings.length === 0) { + // No data. The next lines causes an issue so set specially. + // There may be a better overall fix for no data. + timeReading = 'no data to display'; + size.push(0); + } else { + // only display a range of dates for the hover text if there is more than one day in the range + // Shift to UTC since want database time not local/browser time which is what moment does. + timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`; + if (barDuration.asDays() != 1) { + // subtracting one extra day caused by day ending at midnight of the next day. + // Going from DB unit timestamp that is UTC so force UTC with moment, as usual. + timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`; } - // Use the most recent time reading for the circle on the map. - // This has the limitations of the bar value where the last one can include ranges without - // data (GitHub issue on this). - // TODO: It might be better to do this similarly to compare. (See GitHub issue) - const readings = orderBy(readingsData, ['startTimestamp'], ['desc']); - const mapReading = readings[0]; - let timeReading: string; - let averagedReading = 0; - if (readings.length === 0) { - // No data. The next lines causes an issue so set specially. - // There may be a better overall fix for no data. - timeReading = 'no data to display'; - size.push(0); - } else { - // only display a range of dates for the hover text if there is more than one day in the range - // Shift to UTC since want database time not local/browser time which is what moment does. - timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`; - if (barDuration.asDays() != 1) { - // subtracting one extra day caused by day ending at midnight of the next day. - // Going from DB unit timestamp that is UTC so force UTC with moment, as usual. - timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`; - } - // The value for the circle is the average daily usage. - averagedReading = mapReading.reading / barDuration.asDays(); - if (areaNormalization) { - averagedReading /= meterArea; - } - // The size is the reading value. It will be scaled later. - size.push(averagedReading); + // The value for the circle is the average daily usage. + averagedReading = mapReading.reading / barDuration.asDays(); + if (areaNormalization) { + averagedReading /= meterArea; } - // The hover text. - hoverText.push(`<b> ${timeReading} </b> <br> ${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`); + // The size is the reading value. It will be scaled later. + size.push(averagedReading); } + // The hover text. + hoverText.push(`<b> ${timeReading} </b> <br> ${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`); } } } } + } - for (const groupID of selectedGroups) { - // Get group id number. - // Get group GPS value. - const gps = groupDataById[groupID].gps; - // Filter groups with actual gps coordinates. - if (gps !== undefined && gps !== null && groupData !== undefined) { - let groupArea = groupDataById[groupID].area; - if (!areaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) { - if (areaNormalization) { - // convert the meter area into the proper unit, if needed - groupArea *= getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit); - } - // Convert the gps value to the equivalent Plotly grid coordinates on user map. - // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. - // It must be on true north map since only there are the GPS axis parallel to the map axis. - // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating - // it coordinates on the true north map and then rotating/shifting to the user map. - const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); - // Only display items within valid info and within map. - if (itemMapInfoOk(groupID, DataType.Group, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid)) { - // The x, y value for Plotly to use that are on the user map. - x.push(groupGPSInUserGrid.x); - y.push(groupGPSInUserGrid.y); - // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed - // and be fetching. The unit could change from that menu so also need to check. - // Get the bar data to use for the map circle. - const readingsData = groupData[groupID]; - // This protects against there being no readings or that the data is being updated. - if (readingsData && !groupIsFetching) { - // Group name to include in hover on graph. - const label = groupDataById[groupID].name; - // The usual color for this group. - colors.push(getGraphColor(groupID, DataType.Group)); - if (!readingsData) { - throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + for (const groupID of selectedGroups) { + // Get group id number. + // Get group GPS value. + const gps = groupDataById[groupID].gps; + // Filter groups with actual gps coordinates. + if (gps !== undefined && gps !== null && groupData !== undefined) { + let groupArea = groupDataById[groupID].area; + if (!areaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) { + if (areaNormalization) { + // convert the meter area into the proper unit, if needed + groupArea *= getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit); + } + // Convert the gps value to the equivalent Plotly grid coordinates on user map. + // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. + // It must be on true north map since only there are the GPS axis parallel to the map axis. + // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating + // it coordinates on the true north map and then rotating/shifting to the user map. + const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); + // Only display items within valid info and within map. + if (itemMapInfoOk(groupID, DataType.Group, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid)) { + // The x, y value for Plotly to use that are on the user map. + x.push(groupGPSInUserGrid.x); + y.push(groupGPSInUserGrid.y); + // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed + // and be fetching. The unit could change from that menu so also need to check. + // Get the bar data to use for the map circle. + const readingsData = groupData[groupID]; + // This protects against there being no readings or that the data is being updated. + if (readingsData && !groupIsFetching) { + // Group name to include in hover on graph. + const label = groupDataById[groupID].name; + // The usual color for this group. + colors.push(getGraphColor(groupID, DataType.Group)); + if (!readingsData) { + throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + } + // Use the most recent time reading for the circle on the map. + // This has the limitations of the bar value where the last one can include ranges without + // data (GitHub issue on this). + // TODO: It might be better to do this similarly to compare. (See GitHub issue) + const readings = orderBy(readingsData, ['startTimestamp'], ['desc']); + const mapReading = readings[0]; + let timeReading: string; + let averagedReading = 0; + if (readings.length === 0) { + // No data. The next lines causes an issue so set specially. + // There may be a better overall fix for no data. + timeReading = 'no data to display'; + size.push(0); + } else { + // only display a range of dates for the hover text if there is more than one day in the range + timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`; + if (barDuration.asDays() != 1) { + // subtracting one extra day caused by day ending at midnight of the next day. + // Going from DB unit timestamp that is UTC so force UTC with moment, as usual. + timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`; } - // Use the most recent time reading for the circle on the map. - // This has the limitations of the bar value where the last one can include ranges without - // data (GitHub issue on this). - // TODO: It might be better to do this similarly to compare. (See GitHub issue) - const readings = orderBy(readingsData, ['startTimestamp'], ['desc']); - const mapReading = readings[0]; - let timeReading: string; - let averagedReading = 0; - if (readings.length === 0) { - // No data. The next lines causes an issue so set specially. - // There may be a better overall fix for no data. - timeReading = 'no data to display'; - size.push(0); - } else { - // only display a range of dates for the hover text if there is more than one day in the range - timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`; - if (barDuration.asDays() != 1) { - // subtracting one extra day caused by day ending at midnight of the next day. - // Going from DB unit timestamp that is UTC so force UTC with moment, as usual. - timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`; - } - // The value for the circle is the average daily usage. - averagedReading = mapReading.reading / barDuration.asDays(); - if (areaNormalization) { - averagedReading /= groupArea; - } - // The size is the reading value. It will be scaled later. - size.push(averagedReading); + // The value for the circle is the average daily usage. + averagedReading = mapReading.reading / barDuration.asDays(); + if (areaNormalization) { + averagedReading /= groupArea; } - // The hover text. - hoverText.push(`<b> ${timeReading} </b> <br> ${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`); + // The size is the reading value. It will be scaled later. + size.push(averagedReading); } + // The hover text. + hoverText.push(`<b> ${timeReading} </b> <br> ${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`); } } } } - // TODO Using the following seems to have no impact on the code. It has been noticed that this function is called - // many times for each change. Someone should look at why that is happening and why some have no items in the arrays. - // if (size.length > 0) { - // TODO The max circle diameter should come from admin/DB. - const maxFeatureFraction = map.circleSize; - // Find the smaller of width and height. This is used since it means the circle size will be - // scaled to that dimension and smaller relative to the other coordinate. - const minDimension = Math.min(imageDimensionNormalized.width, imageDimensionNormalized.height); - // The circle size is set to area below. Thus, we need to convert from wanting a max - // diameter of minDimension * maxFeatureFraction to an area. - const maxCircleSize = Math.PI * Math.pow(minDimension * maxFeatureFraction / 2, 2); - // Find the largest circle which is usage. - const largestCircleSize = Math.max(...size); - // Scale largest circle to the max size and others will be scaled to be smaller. - // Not that < 1 => a larger circle. - const scaling = largestCircleSize / maxCircleSize; - - // Per https://plotly.com/javascript/reference/scatter/: - // The opacity of 0.5 makes it possible to see the map even when there is a circle but the hover - // opacity is 1 so it is easy to see. - // Set the sizemode to area not diameter. - // Set the sizemin so a circle cannot get so small that it might disappear. Unsure the best size. - // Set the sizeref to scale each point to the desired area. - // Note all sizes are in px so have to estimate the actual size. This could be an issue but maps are currently - // a fixed size so not too much of an issue. - // Also note that the circle can go off the edge of the map. At some point it would be nice to have a border - // around the map to avoid this. - const traceOne = { - x, - y, - type: 'scatter', - mode: 'markers', - marker: { - color: colors, - opacity: 0.5, - size, - sizemin: 6, - sizeref: scaling, - sizemode: 'area' - }, - text: hoverText, - hoverinfo: 'text', - opacity: 1, - showlegend: false - }; - data.push(traceOne); } + // TODO Using the following seems to have no impact on the code. It has been noticed that this function is called + // many times for each change. Someone should look at why that is happening and why some have no items in the arrays. + // if (size.length > 0) { + // TODO The max circle diameter should come from admin/DB. + const maxFeatureFraction = map.circleSize; + // Find the smaller of width and height. This is used since it means the circle size will be + // scaled to that dimension and smaller relative to the other coordinate. + const minDimension = Math.min(imageDimensionNormalized.width, imageDimensionNormalized.height); + // The circle size is set to area below. Thus, we need to convert from wanting a max + // diameter of minDimension * maxFeatureFraction to an area. + const maxCircleSize = Math.PI * Math.pow(minDimension * maxFeatureFraction / 2, 2); + // Find the largest circle which is usage. + const largestCircleSize = Math.max(...size); + // Scale largest circle to the max size and others will be scaled to be smaller. + // Not that < 1 => a larger circle. + const scaling = largestCircleSize / maxCircleSize; + + // Per https://plotly.com/javascript/reference/scatter/: + // The opacity of 0.5 makes it possible to see the map even when there is a circle but the hover + // opacity is 1 so it is easy to see. + // Set the sizemode to area not diameter. + // Set the sizemin so a circle cannot get so small that it might disappear. Unsure the best size. + // Set the sizeref to scale each point to the desired area. + // Note all sizes are in px so have to estimate the actual size. This could be an issue but maps are currently + // a fixed size so not too much of an issue. + // Also note that the circle can go off the edge of the map. At some point it would be nice to have a border + // around the map to avoid this. + const traceOne = { + x, + y, + type: 'scatter', + mode: 'markers', + marker: { + color: colors, + opacity: 0.5, + size, + sizemin: 6, + sizeref: scaling, + sizemode: 'area' + }, + text: hoverText, + hoverinfo: 'text', + opacity: 1, + showlegend: false + }; + data.push(traceOne); } // set map background image @@ -357,7 +342,7 @@ export default function MapChartComponent() { }, images: [{ layer: 'below', - source: (image) ? image.src : '', + source: map?.mapSource, xref: 'x', yref: 'y', x: 0, @@ -377,4 +362,4 @@ export default function MapChartComponent() { layout={layout} /> ); -} +} \ No newline at end of file diff --git a/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx b/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx index 1fcd0463b..1a7cc4d06 100644 --- a/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx @@ -4,8 +4,12 @@ import * as React from 'react'; import { ChangeEvent } from 'react'; -import { FormattedMessage, WrappedComponentProps, injectIntl } from 'react-intl'; -import { logToServer } from '../../redux/actions/logs'; +import { FormattedMessage } from 'react-intl'; +import { updateMapMode, updateMapSource } from '../../redux/actions/map'; +import { logsApi } from '../../redux/api/logApi'; +import { selectMapById } from '../../redux/api/mapsApi'; +import { useTranslate } from '../../redux/componentHooks'; +import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; import { showErrorNotification } from '../../utils/notifications'; @@ -16,169 +20,167 @@ import { showErrorNotification } from '../../utils/notifications'; * Other configurations could also be selected during this phase; */ -interface MapInitiateProps { - map: MapMetadata - updateMapMode(nextMode: CalibrationModeTypes): any; - onSourceChange(data: MapMetadata): any; -} - -interface MapInitiateState { - filename: string; - mapName: string; - angle: string; -} - -type MapInitiatePropsWithIntl = MapInitiateProps & WrappedComponentProps; - -class MapCalibrationInitiateComponent extends React.Component<MapInitiatePropsWithIntl, MapInitiateState > { - private readonly fileInput: any; - private notifyBadNumber() { - showErrorNotification(`${this.props.intl.formatMessage({id: 'map.bad.number'})}`); - } - private notifyBadDigit360() { - showErrorNotification(`${this.props.intl.formatMessage({id: 'map.bad.digita'})}`); - } - private notifyBadDigit0() { - showErrorNotification(`${this.props.intl.formatMessage({id: 'map.bad.digitb'})}`); - } - private notifyBadMapLoad() { - showErrorNotification(`${this.props.intl.formatMessage({id: 'map.bad.load'})}`); - } - private notifyBadName() { - showErrorNotification(`${this.props.intl.formatMessage({id: 'map.bad.name'})}`); - } - - constructor(props: MapInitiatePropsWithIntl) { - super(props); - this.state = { - filename: '', - mapName: '', - angle: '' - }; - this.fileInput = React.createRef(); - this.handleInput = this.handleInput.bind(this); - this.confirmUpload = this.confirmUpload.bind(this); - this.handleNameInput = this.handleNameInput.bind(this); - this.handleAngleInput = this.handleAngleInput.bind(this); - this.handleAngle = this.handleAngle.bind(this); - this.notifyBadNumber = this.notifyBadNumber.bind(this); - this.notifyBadDigit360 = this.notifyBadDigit360.bind(this); - this.notifyBadDigit0 = this.notifyBadDigit0.bind(this); - this.notifyBadMapLoad = this.notifyBadMapLoad.bind(this); - this.notifyBadName = this.notifyBadName.bind(this); - } - - public render() { - return ( - <form onSubmit={this.confirmUpload}> - <label> - <FormattedMessage id='map.new.upload' /> - <br/> - <input type='file' ref={this.fileInput} /> - </label> - <br /> - <label> - <FormattedMessage id='map.new.name' /> - <br/> - <textarea id={'text'} cols={50} value={this.state.mapName} onChange={this.handleNameInput}/> - </label> - <br/> - <label> - <FormattedMessage id='map.new.angle'/> - <br/> - <input type='text' value={this.state.angle} onChange={this.handleAngleInput}/> - </label> - <br/> - <FormattedMessage id='map.new.submit'> - {placeholder => <input type='submit' value={(placeholder !== null && placeholder !== undefined) ? placeholder.toString() : 'undefined'} />} - </FormattedMessage> - </form> - ); - } - - private async confirmUpload(event: any) { - const bcheck = this.handleAngle(event); +// interface MapInitiateProps { +// map: MapMetadata +// updateMapMode(nextMode: CalibrationModeTypes): any; +// onSourceChange(data: MapMetadata): any; +// } + +// interface MapInitiateState { +// filename: string; +// mapName: string; +// angle: string; +// } + +// type MapInitiatePropsWithIntl = MapInitiateProps & WrappedComponentProps; + +/** + * @returns TODO + */ +export default function MapCalibrationInitiateComponent() { + const translate = useTranslate(); + const [logToServer] = logsApi.useLogToServerMutation(); + const dispatch = useAppDispatch(); + const [mapName, setMapName] = React.useState<string>(''); + const [angle, setAngle] = React.useState<string>(''); + const fileRef = React.useRef<HTMLInputElement>(null); + const mapData = useAppSelector(state => selectMapById(state, state.maps.selectedMap)); + // const [mapData] = useAppSelector(state => selectEntityDisplayData(state, { + // type: EntityType.MAP, + // id: state.localEdits.mapCalibration.calibratingMap + // })); + + + const notify = (key: 'map.bad.number' | 'map.bad.digita' | 'map.bad.digitb' | 'map.bad.load' | 'map.bad.name') => { + showErrorNotification(translate(key)); + }; + const confirmUpload = async (event: React.FormEvent<HTMLFormElement>) => { + const bcheck = handleAngle(event); if (bcheck) { - if (this.fileInput.current.files.length === 0) { - this.notifyBadMapLoad(); + if (!fileRef.current?.files || fileRef.current.files.length === 0) { + notify('map.bad.load'); } - else if (this.state.mapName.trim() === '') { - this.notifyBadName(); + else if (mapName.trim() === '') { + notify('map.bad.name'); } else { - await this.handleInput(event); - this.props.updateMapMode(CalibrationModeTypes.calibrate); + await processImgUpload(event); } } - } + }; - private handleAngle(event: any) { + const handleAngle = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); const pattern = /^[-+]?\d+(\.\d+)?$/; - if (!pattern.test(this.state.angle)) { - this.notifyBadNumber(); + if (!pattern.test(angle)) { + notify('map.bad.number'); + return false; } else { - if (parseFloat(this.state.angle) > 360) { - this.notifyBadDigit360(); + if (parseFloat(angle) > 360) { + notify('map.bad.digita'); return false; } - else if (parseFloat(this.state.angle) < 0) { - this.notifyBadDigit0(); + else if (parseFloat(angle) < 0) { + notify('map.bad.digitb'); return false; } else { return true; } } - } + }; - private async handleInput(event: any) { + const processImgUpload = async (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); try { - const imageURL = await this.getDataURL(); - this.setState({filename: this.fileInput.current.files[0].name}); - const image = new Image(); - image.src = imageURL; - const source: MapMetadata = { - ...this.props.map, - name: this.state.mapName, - filename: this.fileInput.current.files[0].name, - image, - northAngle: parseFloat(this.state.angle) - }; - await this.props.onSourceChange(source); + const mapMetaData = await processImgMapMetaData(); + dispatch(updateMapSource(mapMetaData)); + dispatch(updateMapMode(CalibrationModeTypes.calibrate)); } catch (err) { - logToServer('error', `Error, map source image uploading: ${err}`)(); + logToServer({ level: 'error', message: `Error, map source image uploading: ${err}` }); } - } + }; - private handleNameInput(event: ChangeEvent<HTMLTextAreaElement>) { - this.setState({ - mapName: event.target.value - }); - } + const handleNameInput = (event: ChangeEvent<HTMLTextAreaElement>) => { setMapName(event.target.value); }; - private handleAngleInput(event: React.FormEvent<HTMLInputElement>) { - this.setState({ - angle: event.currentTarget.value - }); - } + const handleAngleInput = (event: React.FormEvent<HTMLInputElement>) => { setAngle(event.currentTarget.value); }; - private getDataURL(): Promise<string> { + // Takes image from upload, derives dimensions, and generates MapMetaData Object for redux state. + // No longer using Image element in Redux state for serializability purposes. Store img.src only. + const processImgMapMetaData = (): Promise<MapMetadata> => { return new Promise((resolve, reject) => { - const file = this.fileInput.current.files[0]; - const fileReader = new FileReader(); - fileReader.onloadend = () => { - if (typeof fileReader.result === 'string') { - resolve(fileReader.result); - } - }; - fileReader.onerror = reject; - fileReader.readAsDataURL(file); + const file = fileRef.current?.files?.[0]; + if (!file) { + reject('No File Found'); + + } else { + + const fileReader = new FileReader(); + // Fire when loading complete + fileReader.onloadend = () => { + // When file upload completed, use the result to create an image + // use image, to extract image dimensions; + if (typeof fileReader.result === 'string') { + img.src = fileReader.result; + } + }; + fileReader.onerror = reject; + // begin file read + fileReader.readAsDataURL(file); + const img = new Image(); + // Fire when image load complete. + img.onload = () => { + // resolve mapMetadata from image. + // Not storing image in state, instead extract relevang values + resolve({ + ...mapData, + imgWidth: img.width, + imgHeight: img.height, + filename: file.name, + name: mapName, + northAngle: parseFloat(angle), + // Save the image source only + // Does not store the Image Obpect in redux for serializability reasons. + // use mapSource to recreate images when needed. + mapSource: img.src + }); + + }; + // file when image error + img.onerror = error => { + reject(error); + }; + + } + }); - } -} + }; -export default injectIntl(MapCalibrationInitiateComponent); + return ( + <form onSubmit={confirmUpload}> + <label> + <FormattedMessage id='map.new.upload' /> + <br /> + <input type='file' ref={fileRef} /> + </label> + <br /> + <label> + <FormattedMessage id='map.new.name' /> + <br /> + <textarea id={'text'} cols={50} value={mapName} onChange={handleNameInput} /> + </label> + <br /> + <label> + <FormattedMessage id='map.new.angle' /> + <br /> + <input type='text' value={angle} onChange={handleAngleInput} /> + </label> + <br /> + <FormattedMessage id='map.new.submit'> + {placeholder => <input type='submit' value={(placeholder !== null && placeholder !== undefined) ? placeholder.toString() : 'undefined'} />} + </FormattedMessage> + </form> + ); +} \ No newline at end of file diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index 4156ef9bc..937f65430 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -2,16 +2,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { parseZone } from 'moment'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { parseZone } from 'moment'; -import '../../styles/card-page.css'; -import EditMapModalComponent from './EditMapModalComponent'; -import { selectMapById } from '../../redux/selectors/maps'; +import { LocaleDataKey } from 'translations/data'; import { useAppSelector } from '../../redux/reduxHooks'; -import { MapMetadata } from 'types/redux/map'; +import { selectMapById } from '../../redux/selectors/maps'; +import '../../styles/card-page.css'; import translate from '../../utils/translate'; -import { LocaleDataKey } from 'translations/data'; +import EditMapModalComponent from './EditMapModalComponent'; interface MapViewProps { mapID: number; } @@ -19,7 +18,7 @@ interface MapViewProps { //TODO: Migrate to RTK const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { - const map: MapMetadata = useAppSelector(selectMapById(mapID)); + const map = useAppSelector(selectMapById(mapID)); // Helper function checks map to see if it's calibrated const getCalibrationStatus = () => { diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index 5d6d8183c..c5a34ef17 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -6,14 +6,13 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; import { Button } from 'reactstrap'; +import { selectMapIds } from '../../redux/api/mapsApi'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import MapViewComponent from './MapViewComponent'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import '../../styles/card-page.css'; -import { fetchMapsDetails, setNewMap } from '../../redux/actions/map'; +import { setNewMap } from '../../redux/actions/map'; import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; -import { selectMaps } from '../../redux/selectors/maps'; -import { AppDispatch } from 'store'; +import '../../styles/card-page.css'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import MapViewComponent from './MapViewComponent'; /** * Defines the maps page card view @@ -21,13 +20,9 @@ import { AppDispatch } from 'store'; */ // TODO: Migrate to RTK export default function MapsDetailComponent() { - const dispatch: AppDispatch = useAppDispatch(); + const dispatch = useAppDispatch(); // Load map IDs from state and store in number array - const maps: number[] = useAppSelector(selectMaps); - React.useEffect(() => { - // Load maps from state on component mount (componentDidMount) - dispatch(fetchMapsDetails()); - }, []); + const maps = useAppSelector(state => selectMapIds(state)); return ( <div className='flexGrowOne'> diff --git a/src/client/app/containers/MapChartContainer.ts b/src/client/app/containers/MapChartContainer.ts index 45a8049c6..28320c022 100644 --- a/src/client/app/containers/MapChartContainer.ts +++ b/src/client/app/containers/MapChartContainer.ts @@ -31,7 +31,6 @@ function mapStateToProps(state: State) { // Holds Plotly mapping info. const data = []; // Holds the image to use. - let image; if (state.maps.selectedMap !== 0) { const mapID = state.maps.selectedMap; if (state.maps.byMapID[mapID]) { @@ -48,7 +47,6 @@ function mapStateToProps(state: State) { const colors: string[] = []; // If there is no map then use a new, empty image as the map. I believe this avoids errors // and gives the blank screen. - image = (map) ? map.image : new Image(); // Arrays to hold the Plotly grid location (x, y) for circles to place on map. const x: number[] = []; const y: number[] = []; @@ -60,8 +58,8 @@ function mapStateToProps(state: State) { if (map && map.origin && map.opposite) { // The size of the original map loaded into OED. const imageDimensions: Dimensions = { - width: image.width, - height: image.height + width: map.imgWidth, + height: map.imgHeight }; // Determine the dimensions so within the Plotly coordinates on the user map. const imageDimensionNormalized = normalizeImageDimensions(imageDimensions); @@ -325,7 +323,7 @@ function mapStateToProps(state: State) { }, images: [{ layer: 'below', - source: (image) ? image.src : '', + source: map?.mapSource, xref: 'x', yref: 'y', x: 0, diff --git a/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts b/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts index 6dfcd905a..64208c7fe 100644 --- a/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts +++ b/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts @@ -28,8 +28,8 @@ function mapStateToProps(state: State) { } } const imageDimensions: Dimensions = normalizeImageDimensions({ - width: map.image.width, - height: map.image.height + width: map.imgWidth, + height: map.imgHeight }); const settings = state.maps.calibrationSettings; const backgroundTrace = createBackgroundTrace(imageDimensions, settings); @@ -49,7 +49,7 @@ function mapStateToProps(state: State) { }; const data = [backgroundTrace, dataPointTrace]; - const imageSource = map.image.src; + const imageSource = map.mapSource; // for a detailed description of layout attributes: https://plotly.com/javascript/reference/#layout const layout: any = { diff --git a/src/client/app/redux/actions/map.ts b/src/client/app/redux/actions/map.ts index 5af42df04..688183839 100644 --- a/src/client/app/redux/actions/map.ts +++ b/src/client/app/redux/actions/map.ts @@ -80,7 +80,7 @@ export function setCalibration(mode: CalibrationModeTypes, mapID: number): Thunk }; } -function prepareCalibration(mode: CalibrationModeTypes, mapID: number): t.SetCalibrationAction { +export function prepareCalibration(mode: CalibrationModeTypes, mapID: number): t.SetCalibrationAction { return { type: ActionType.SetCalibration, mode, mapID }; } @@ -190,8 +190,8 @@ function prepareDataToCalculation(state: State): CalibrationResult { const mapID = state.maps.calibratingMap; const mp = state.maps.editedMaps[mapID]; const imageDimensions: Dimensions = { - width: mp.image.width, - height: mp.image.height + width: mp.imgWidth, + height: mp.imgHeight }; // Since mp is defined above, calibrationSet is defined. /* eslint-disable @typescript-eslint/no-non-null-assertion */ @@ -240,11 +240,11 @@ export function submitNewMap(): Thunk { try { const acceptableMap: MapData = { ...map, - mapSource: map.image.src, + mapSource: map.mapSource, displayable: false, modifiedDate: moment().toISOString(), - origin: (map.calibrationResult) ? map.calibrationResult.origin : undefined, - opposite: (map.calibrationResult) ? map.calibrationResult.opposite : undefined + origin: map.calibrationResult?.origin, + opposite: map.calibrationResult?.opposite }; await mapsApi.create(acceptableMap); if (map.calibrationResult) { @@ -274,15 +274,15 @@ export function submitEditedMap(mapID: number): Thunk { try { const acceptableMap: MapData = { ...map, - mapSource: map.image.src, + mapSource: map.mapSource, // As in other place, this take the time, in this case the current time, grabs the // date and time without timezone and then set it to UTC. This allows the software // to recreate it with the same date/time as it is on this web browser when it is // displayed later (without the timezone shown). // It might be better to use the server time but this is good enough. modifiedDate: moment().format('YYYY-MM-DD HH:mm:ss') + '+00:00', - origin: (map.calibrationResult) ? map.calibrationResult.origin : map.origin, - opposite: (map.calibrationResult) ? map.calibrationResult.opposite : map.opposite, + origin: map.calibrationResult?.origin, + opposite: map.calibrationResult?.opposite, circleSize: map.circleSize }; await mapsApi.edit(acceptableMap); diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index 58d58b5e1..3750dfe3f 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -27,6 +27,7 @@ export const baseApi = createApi({ // The types of tags that any injected endpoint may, provide, or invalidate. // Must be defined here, for use in injected endpoints tagTypes: [ + 'MapsData', 'MeterData', 'GroupData', 'GroupChildrenData', diff --git a/src/client/app/redux/api/logApi.ts b/src/client/app/redux/api/logApi.ts new file mode 100644 index 000000000..f601e469a --- /dev/null +++ b/src/client/app/redux/api/logApi.ts @@ -0,0 +1,14 @@ +import { LogData } from 'types/redux/logs'; +import { baseApi } from './baseApi'; + +export const logsApi = baseApi.injectEndpoints({ + endpoints: builder => ({ + logToServer: builder.mutation<void, LogData & { level: 'info' | 'warn' | 'error' }>({ + query: ({ level, ...logData }) => ({ + url: `api/logs/${level}`, + method: 'POST', + body: logData + }) + }) + }) +}); \ No newline at end of file diff --git a/src/client/app/redux/api/mapsApi.ts b/src/client/app/redux/api/mapsApi.ts new file mode 100644 index 000000000..9f1afcd21 --- /dev/null +++ b/src/client/app/redux/api/mapsApi.ts @@ -0,0 +1,147 @@ +import { createEntityAdapter, EntityState } from '@reduxjs/toolkit'; +import { pick } from 'lodash'; +import * as moment from 'moment'; +import { RootState } from '../../store'; +import { MapData, MapMetadata } from '../../types/redux/map'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; +import translate from '../../utils/translate'; +import { baseApi } from './baseApi'; +// import { logToServer } from '../../redux/actions/logs'; + +// Helper function to extract image dimensions from the mapSource +const mapResponseImgSrcToDimensions = (response: MapMetadata[]) => Promise.all( + response.map(mapData => + new Promise<MapMetadata>(resolve => { + const img = new Image(); + img.onload = () => { + resolve({ ...mapData, imgWidth: img.width, imgHeight: img.height }); + }; + img.onerror = () => { + // TODO default to falsy value, 0, on error. + resolve({ ...mapData, imgWidth: 0, imgHeight: 0 }); + }; + img.src = mapData.mapSource; + }) + ) +); + + +export const mapsAdapter = createEntityAdapter<MapMetadata>({ + sortComparer: (meterA, meterB) => meterA.name?.localeCompare(meterB.name, undefined, { sensitivity: 'accent' }) + +}); +export const mapsInitialState = mapsAdapter.getInitialState(); +export type MapDataState = EntityState<MapMetadata, number>; + + +export const mapsApi = baseApi.injectEndpoints({ + endpoints: build => ({ + getMapDetails: build.query<MapDataState, void>({ + query: () => 'api/maps/', + transformResponse: async (response: MapMetadata[]) => { + // To avoid saving unserializable image(s) in state, extract the image dimensions and only store the mapSource (string) + return mapsAdapter.setAll(mapsInitialState, await mapResponseImgSrcToDimensions(response)); + }, + providesTags: ['MapsData'] + }), + getMapByName: build.query<MapData, string>({ + query: name => ({ + url: 'api/maps/getByName', + params: { name } + }) + }), + createMap: build.mutation<void, MapMetadata>({ + query: map => ({ + url: 'api/maps/create', + method: 'POST', + body: { + // send only what backend expects. + ...pick(map, ['name', 'note', 'filename', 'mapSource', 'northAngle', 'circleSize']), + modifiedDate: moment().toISOString(), + origin: (map.calibrationResult) ? map.calibrationResult.origin : undefined, + opposite: (map.calibrationResult) ? map.calibrationResult.opposite : undefined + } + }), + onQueryStarted: (map, api) => { + api.queryFulfilled + // TODO Serverlogs migrate to rtk Query to drop axios? + // Requires dispatch so inconvenient + .then(() => { + if (map.calibrationResult) { + // logToServer('info', 'New calibrated map uploaded to database'); + showSuccessNotification(translate('upload.new.map.with.calibration')); + } else { + // logToServer('info', 'New map uploaded to database(without calibration)'); + showSuccessNotification(translate('upload.new.map.without.calibration')); + } + // TODO DELETE ME + // api.dispatch(localEditsSlice.actions.removeOneEdit({ type: EntityType.MAP, id: map.id })); + }).catch(() => { + showErrorNotification(translate('failed.to.edit.map')); + }); + }, + invalidatesTags: ['MapsData'] + }), + editMap: build.mutation<MapData, MapMetadata>({ + query: map => ({ + url: 'api/maps/edit', + method: 'POST', + body: { + // send only what backend expects. + ...pick(map, ['id', 'name', 'displayable', 'note', 'filename', 'mapSource', 'northAngle', 'circleSize']), + // As in other place, this take the time, in this case the current time, grabs the + // date and time without timezone and then set it to UTC. This allows the software + // to recreate it with the same date/time as it is on this web browser when it is + // displayed later (without the timezone shown). + // It might be better to use the server time but this is good enough. + modifiedDate: moment().format('YYYY-MM-DD HH:mm:ss') + '+00:00', + origin: map.calibrationResult ? map.calibrationResult.origin : map.origin, + opposite: map.calibrationResult ? map.calibrationResult.opposite : map.opposite + } + }), + onQueryStarted: (map, api) => { + api.queryFulfilled + // TODO Serverlogs migrate to rtk Query to drop axios? + // Requires dispatch so inconvenient + .then(() => { + if (map.calibrationResult) { + // logToServer('info', 'Edited map uploaded to database(newly calibrated)'); + showSuccessNotification(translate('updated.map.with.calibration')); + } else if (map.origin && map.opposite) { + // logToServer('info', 'Edited map uploaded to database(calibration not updated)'); + showSuccessNotification(translate('updated.map.without.new.calibration')); + } else { + // logToServer('info', 'Edited map uploaded to database(without calibration)'); + showSuccessNotification(translate('updated.map.without.calibration')); + } + // Cleanup LocalEditsSLice + // TODO Centralize localEditCleanup. Should be same as others. + // api.dispatch(localEditsSlice.actions.removeOneEdit({ type: EntityType.MAP, id: map.id })); + }).catch(() => { + showErrorNotification(translate('failed.to.edit.map')); + }); + }, + invalidatesTags: ['MapsData'] + }), + deleteMap: build.mutation<void, number>({ + query: id => ({ + url: 'api/maps/delete', + method: 'POST', + body: { id } + }) + }), + getMapById: build.query<MapData, number>({ + query: id => `api/maps/${id}` + }) + }) +}); + +const selectMapDataResult = mapsApi.endpoints.getMapDetails.select(); +export const selectMapApiData = (state: RootState) => selectMapDataResult(state).data ?? mapsInitialState; +export const { + selectAll: selectAllMaps, + selectById: selectMapById, + selectIds: selectMapIds, + selectEntities: selectMapDataById, + selectTotal: selectTotalMaps +} = mapsAdapter.getSelectors(selectMapApiData); diff --git a/src/client/app/redux/devToolConfig.ts b/src/client/app/redux/devToolConfig.ts new file mode 100644 index 000000000..70d5a31b0 --- /dev/null +++ b/src/client/app/redux/devToolConfig.ts @@ -0,0 +1,21 @@ +import { DevToolsEnhancerOptions } from '@reduxjs/toolkit'; +import { mapsAdapter, mapsApi, mapsInitialState } from './api/mapsApi'; + +export const devToolsConfig: DevToolsEnhancerOptions = { + actionSanitizer: action => { + switch (true) { + // Sanitize MapSource so it does not bloat the devtools with a longBlobs. + case mapsApi.endpoints.getMapDetails.matchFulfilled(action): { + // omitMapSource from metaData + const sanitizedMapMetadata = Object.values(action.payload.entities) + .map(data => ({ ...data, mapSource: 'Omitted From Devtools Serialization' })); + + // sanitized devtool Action + return { ...action, payload: { ...mapsAdapter.setAll(mapsInitialState, sanitizedMapMetadata) } }; + } + default: + return action; + } + + } +}; \ No newline at end of file diff --git a/src/client/app/redux/reducers/maps.ts b/src/client/app/redux/reducers/maps.ts index b43a0e789..3cd683941 100644 --- a/src/client/app/redux/reducers/maps.ts +++ b/src/client/app/redux/reducers/maps.ts @@ -123,7 +123,8 @@ export default function maps(state = defaultState, action: MapsAction) { const mapToReset = { ...editedMaps[action.mapID] }; delete mapToReset.currentPoint; delete mapToReset.calibrationResult; - delete mapToReset.calibrationSet; + // TODO FIX mapsDataFetch + // delete mapToReset.calibrationSet; return { ...state, editedMaps: { diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 0eeff547a..b80edd585 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -147,11 +147,10 @@ export const selectChartTypeCompatibility = createAppSelector( if (chartToRender === ChartTypes.map && mapState.selectedMap !== 0) { const mp = mapState.byMapID[mapState.selectedMap]; // filter meters; - const image = mp.image; // The size of the original map loaded into OED. const imageDimensions: Dimensions = { - width: image.width, - height: image.height + width: mp.imgWidth, + height: mp.imgHeight }; // Determine the dimensions so within the Plotly coordinates on the user map. const imageDimensionNormalized = normalizeImageDimensions(imageDimensions); diff --git a/src/client/app/redux/slices/appStateSlice.ts b/src/client/app/redux/slices/appStateSlice.ts index edcc479e9..8099e5def 100644 --- a/src/client/app/redux/slices/appStateSlice.ts +++ b/src/client/app/redux/slices/appStateSlice.ts @@ -3,6 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as moment from 'moment'; +import { processGraphLink } from '../../redux/actions/extraActions'; +import { mapsApi } from '../../redux/api/mapsApi'; import { LanguageTypes } from '../../types/redux/i18n'; import { deleteToken, getToken, hasToken } from '../../utils/token'; import { fetchMapsDetails } from '../actions/map'; @@ -16,7 +18,6 @@ import { userApi } from '../api/userApi'; import { versionApi } from '../api/versionApi'; import { createThunkSlice } from '../sliceCreators'; import { currentUserSlice } from './currentUserSlice'; -import { processGraphLink } from '../../redux/actions/extraActions'; export interface AppState { initComplete: boolean; @@ -96,6 +97,8 @@ export const appStateSlice = createThunkSlice({ // Request meter/group/details post-auth dispatch(metersApi.endpoints.getMeters.initiate()); dispatch(groupsApi.endpoints.getGroups.initiate()); + dispatch(mapsApi.endpoints.getMapDetails.initiate()); + }, { settled: state => { diff --git a/src/client/app/store.ts b/src/client/app/store.ts index 5d227d250..3a1cce927 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -3,20 +3,22 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { configureStore } from '@reduxjs/toolkit'; -import { rootReducer } from './redux/rootReducer'; +import { setGlobalDevModeChecks } from 'reselect'; import { baseApi } from './redux/api/baseApi'; -import { Dispatch } from './types/redux/actions'; +import { devToolsConfig } from './redux/devToolConfig'; import { listenerMiddleware } from './redux/listenerMiddleware'; -import { setGlobalDevModeChecks } from 'reselect'; +import { rootReducer } from './redux/rootReducer'; +import { Dispatch } from './types/redux/actions'; export const store = configureStore({ reducer: rootReducer, middleware: getDefaultMiddleware => getDefaultMiddleware({ immutableCheck: false, serializableCheck: false - }) - .prepend(listenerMiddleware.middleware) - .concat(baseApi.middleware) + }).prepend(listenerMiddleware.middleware) + .concat(baseApi.middleware), + devTools: devToolsConfig + }); // stability check for ALL createSelector instances. diff --git a/src/client/app/types/redux/map.ts b/src/client/app/types/redux/map.ts index d1700c035..5a07976bb 100644 --- a/src/client/app/types/redux/map.ts +++ b/src/client/app/types/redux/map.ts @@ -145,6 +145,7 @@ export interface MapData { * @param name * @param displayable */ + export interface MapMetadata { id: number; name: string; @@ -154,13 +155,16 @@ export interface MapMetadata { modifiedDate: string; origin?: GPSPoint; opposite?: GPSPoint; - image: HTMLImageElement; + mapSource: string; + northAngle: number; + circleSize: number; + // image: HTMLImageElement; + imgHeight: number; + imgWidth: number; calibrationMode?: CalibrationModeTypes; currentPoint?: CalibratedPoint; - calibrationSet?: CalibratedPoint[]; + calibrationSet: CalibratedPoint[]; calibrationResult?: CalibrationResult; - northAngle: number; - circleSize: number; } /** From 45d0db6253b4e1b2e974f1ba1833392927f361cf Mon Sep 17 00:00:00 2001 From: ChrisMart21 <ChrisMart21@github.com> Date: Sun, 11 Aug 2024 08:57:16 -0700 Subject: [PATCH 38/50] Remove Maps from RootState - maps no longer a state property, - old State type deleted, only use RootState now - admin pages untested, graph seems okay. - needs testing. --- .../app/components/ChartSelectComponent.tsx | 17 +- .../app/components/MapChartComponent.tsx | 3 +- .../components/MapChartSelectComponent.tsx | 28 ++- src/client/app/components/RouteComponent.tsx | 10 +- .../MapCalibrationChartDisplayComponent.tsx | 142 ++++++++++++++ .../maps/MapCalibrationComponent.tsx | 36 ++++ .../MapCalibrationInfoDisplayComponent.tsx | 178 +++++++++--------- .../maps/MapCalibrationInitiateComponent.tsx | 8 +- .../app/components/maps/MapViewComponent.tsx | 4 +- src/client/app/redux/actions/map.ts | 16 +- src/client/app/redux/api/baseApi.ts | 4 - src/client/app/redux/api/mapsApi.ts | 10 +- src/client/app/redux/rootReducer.ts | 8 +- .../redux/selectors/chartQuerySelectors.ts | 16 +- src/client/app/redux/selectors/maps.ts | 19 -- src/client/app/redux/selectors/uiSelectors.ts | 38 ++-- src/client/app/redux/slices/graphSlice.ts | 22 ++- .../app/redux/slices/localEditsSlice.ts | 122 ++++++++++++ src/client/app/types/redux/graph.ts | 1 + src/client/app/types/redux/state.ts | 37 ---- src/client/app/utils/calibration.ts | 15 +- 21 files changed, 493 insertions(+), 241 deletions(-) create mode 100644 src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx delete mode 100644 src/client/app/redux/selectors/maps.ts create mode 100644 src/client/app/redux/slices/localEditsSlice.ts delete mode 100644 src/client/app/types/redux/state.ts diff --git a/src/client/app/components/ChartSelectComponent.tsx b/src/client/app/components/ChartSelectComponent.tsx index 9625734ae..296d37048 100644 --- a/src/client/app/components/ChartSelectComponent.tsx +++ b/src/client/app/components/ChartSelectComponent.tsx @@ -2,17 +2,13 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { sortBy, values } from 'lodash'; import * as React from 'react'; import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useSelector } from 'react-redux'; import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { graphSlice, selectChartToRender } from '../redux/slices/graphSlice'; -import { SelectOption } from '../types/items'; import { ChartTypes } from '../types/redux/graph'; -import { State } from '../types/redux/state'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; @@ -24,10 +20,10 @@ export default function ChartSelectComponent() { const currentChartToRender = useAppSelector(selectChartToRender); const dispatch = useAppDispatch(); const [expand, setExpand] = useState(false); - const mapsById = useSelector((state: State) => state.maps.byMapID); - const sortedMaps = sortBy(values(mapsById).map(map => ( - { value: map.id, label: map.name, isDisabled: !(map.origin && map.opposite) } as SelectOption - )), 'label'); + // const mapsById = useAppSelector(selectMapDataById); + // const sortedMaps = sortBy(values(mapsById).map(map => ( + // { value: map.id, label: map.name, isDisabled: !(map.origin && map.opposite) } as SelectOption + // )), 'label'); return ( <> @@ -52,11 +48,6 @@ export default function ChartSelectComponent() { key={chartType} onClick={() => { dispatch(graphSlice.actions.changeChartToRender(chartType)); - if (chartType === ChartTypes.map && Object.keys(sortedMaps).length === 1) { - // If there is only one map, selectedMap is the id of the only map. ie; display map automatically if only 1 map - dispatch({ type: 'UPDATE_SELECTED_MAPS', mapID: sortedMaps[0].value }); - - } }} > {translate(`${chartType}`)} diff --git a/src/client/app/components/MapChartComponent.tsx b/src/client/app/components/MapChartComponent.tsx index 7ce9d34b8..8d23dd1bd 100644 --- a/src/client/app/components/MapChartComponent.tsx +++ b/src/client/app/components/MapChartComponent.tsx @@ -8,6 +8,7 @@ import * as React from 'react'; import { selectAreaUnit, selectBarWidthDays, selectGraphAreaNormalization, selectSelectedGroups, + selectSelectedMap, selectSelectedMeters, selectSelectedUnit } from '../redux/slices/graphSlice'; import { selectGroupDataById } from '../redux/api/groupsApi'; @@ -56,7 +57,7 @@ export default function MapChartComponent() { // RTK Types Disagree with maps ts types so, use old until migration completer for maps. // This is also an issue when trying to refactor maps reducer into slice. - const selectedMap = useAppSelector(state => state.maps.selectedMap); + const selectedMap = useAppSelector(selectSelectedMap); const map = useAppSelector(state => selectMapById(state, selectedMap)); if (meterIsFetching || groupIsFetching) { diff --git a/src/client/app/components/MapChartSelectComponent.tsx b/src/client/app/components/MapChartSelectComponent.tsx index bea22af7f..4504c1847 100644 --- a/src/client/app/components/MapChartSelectComponent.tsx +++ b/src/client/app/components/MapChartSelectComponent.tsx @@ -3,11 +3,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { sortBy, values } from 'lodash'; -import { useDispatch, useSelector } from 'react-redux'; -import { State } from '../types/redux/state'; -import { SelectOption } from '../types/items'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { selectMapById, selectMapSelectOptions } from '../redux/api/mapsApi'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +import { selectSelectedMap, updateSelectedMaps } from '../redux/slices/graphSlice'; import SingleSelectComponent from './SingleSelectComponent'; import TooltipMarkerComponent from './TooltipMarkerComponent'; @@ -24,19 +23,20 @@ export default function MapChartSelectComponent() { margin: 0 }; const messages = defineMessages({ - selectMap: {id: 'select.map'} + selectMap: { id: 'select.map' } }); // TODO When this is converted to RTK then should use useAppDispatch(). //Utilizes useDispatch and useSelector hooks - const dispatch = useDispatch(); - const sortedMaps = sortBy(values(useSelector((state: State) => state.maps.byMapID)).map(map => ( - { value: map.id, label: map.name, isDisabled: !(map.origin && map.opposite) } as SelectOption - )), 'label'); + const dispatch = useAppDispatch(); + + const sortedMaps = useAppSelector(selectMapSelectOptions); + const selectedMapData = useAppSelector(state => selectMapById(state, selectSelectedMap(state))); + const selectedMap = { - label: useSelector((state: State) => state.maps.byMapID[state.maps.selectedMap] ? state.maps.byMapID[state.maps.selectedMap].name : ''), - value: useSelector((state: State) => state.maps.selectedMap) + label: selectedMapData.name, + value: selectedMapData.id }; //useIntl instead of injectIntl and WrappedComponentProps @@ -46,16 +46,14 @@ export default function MapChartSelectComponent() { <div> <p style={labelStyle}> <FormattedMessage id='maps' />: - <TooltipMarkerComponent page='home' helpTextId='help.home.select.maps'/> + <TooltipMarkerComponent page='home' helpTextId='help.home.select.maps' /> </p> <div style={divBottomPadding}> <SingleSelectComponent options={sortedMaps} selectedOption={(selectedMap.value === 0) ? undefined : selectedMap} placeholder={intl.formatMessage(messages.selectMap)} - onValueChange={selected => dispatch({type: 'UPDATE_SELECTED_MAPS', mapID: selected.value})} - //When we specify stuff in actions files, we also specify other variables, in this case mapID. - //This is where we specify values instead of triggering the action by itself. + onValueChange={selected => dispatch(updateSelectedMaps(selected.value))} /> </div> </div> diff --git a/src/client/app/components/RouteComponent.tsx b/src/client/app/components/RouteComponent.tsx index 1f9ec4e13..827645741 100644 --- a/src/client/app/components/RouteComponent.tsx +++ b/src/client/app/components/RouteComponent.tsx @@ -5,9 +5,8 @@ import * as React from 'react'; import { IntlProvider } from 'react-intl'; import { RouterProvider, createBrowserRouter } from 'react-router-dom'; import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; -import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; -import MapsDetailComponent from './maps/MapsDetailComponent'; import { useAppSelector } from '../redux/reduxHooks'; +import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; import LocaleTranslationData from '../translations/data'; import { UserRole } from '../types/items'; import AppLayout from './AppLayout'; @@ -17,14 +16,15 @@ import AdminComponent from './admin/AdminComponent'; import UsersDetailComponent from './admin/users/UsersDetailComponent'; import ConversionsDetailComponent from './conversion/ConversionsDetailComponent'; import GroupsDetailComponent from './groups/GroupsDetailComponent'; +import { MapCalibrationComponent2 } from './maps/MapCalibrationComponent'; +import MapsDetailComponent from './maps/MapsDetailComponent'; import MetersDetailComponent from './meters/MetersDetailComponent'; import AdminOutlet from './router/AdminOutlet'; +import ErrorComponent from './router/ErrorComponent'; import { GraphLink } from './router/GraphLinkComponent'; import NotFound from './router/NotFoundOutlet'; import RoleOutlet from './router/RoleOutlet'; import UnitsDetailComponent from './unit/UnitsDetailComponent'; -import ErrorComponent from './router/ErrorComponent'; -import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; /** * @returns the router component Responsible for client side routing. @@ -54,7 +54,7 @@ const router = createBrowserRouter([ element: <AdminOutlet />, children: [ { path: 'admin', element: <AdminComponent /> }, - { path: 'calibration', element: <MapCalibrationContainer /> }, + { path: 'calibration', element: <MapCalibrationComponent2 /> }, { path: 'maps', element: <MapsDetailComponent /> }, { path: 'units', element: <UnitsDetailComponent /> }, { path: 'conversions', element: <ConversionsDetailComponent /> }, diff --git a/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx b/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx new file mode 100644 index 000000000..a2f0e071a --- /dev/null +++ b/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx @@ -0,0 +1,142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { PlotData, PlotMouseEvent } from 'plotly.js'; +import * as React from 'react'; +import Plot from 'react-plotly.js'; +import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; +import { selectSelectedLanguage } from '../../redux/slices/appStateSlice'; +import { localEditsSlice } from '../../redux/slices/localEditsSlice'; +import Locales from '../../types/locales'; +import { CalibrationSettings } from '../../types/redux/map'; +import { Dimensions, normalizeImageDimensions } from '../../utils/calibration'; +import { mapsAdapter } from '../../redux/api/mapsApi'; + +/** + * @returns TODO DO ME + */ +export default function MapCalibrationChartDisplayContainer() { + const dispatch = useAppDispatch(); + const x: number[] = []; + const y: number[] = []; + const texts: string[] = []; + const currentLanguange = useAppSelector(selectSelectedLanguage); + const map = useAppSelector(state => mapsAdapter.getSelectors().selectById(state.localEdits.mapEdits, state.localEdits.calibratingMap)); + + const settings = useAppSelector(state => state.localEdits.calibrationSettings); + const points = map.calibrationSet; + if (points) { + for (const point of points) { + x.push(point.cartesian.x); + y.push(point.cartesian.y); + texts.push(`latitude: ${point.gps.latitude}, longitude: ${point.gps.longitude}`); + } + } + const imageDimensions: Dimensions = normalizeImageDimensions({ + width: map.imgWidth, + height: map.imgHeight + }); + const backgroundTrace = createBackgroundTrace(imageDimensions, settings); + const dataPointTrace = { + x, + y, + type: 'scatter', + mode: 'markers', + marker: { + color: 'rgb(7,110,180)', + opacity: 0.5, + size: 6 + }, + text: texts, + opacity: 1, + showlegend: false + }; + const data = [backgroundTrace, dataPointTrace]; + + const imageSource = map.mapSource; + + // for a detailed description of layout attributes: https://plotly.com/javascript/reference/#layout + const layout: any = { + width: 1000, + height: 1000, + xaxis: { + visible: false, // changes all visibility settings including showgrid, zeroline, showticklabels and hiding ticks + range: [0, 500] // range of displayed graph + }, + yaxis: { + visible: false, + range: [0, 500], + scaleanchor: 'x' + }, + images: [{ + layer: 'below', + source: imageSource, + xref: 'x', + yref: 'y', + x: 0, + y: 0, + sizex: 500, + sizey: 500, + xanchor: 'left', + yanchor: 'bottom', + sizing: 'contain', + opacity: 1 + }] + }; + + return <Plot + data={data as PlotData[]} + layout={layout} + config={{ + // makes locales available for use + locales: Locales, + locale: currentLanguange + }} + onClick={(event: PlotMouseEvent) => { + event.event.preventDefault(); + dispatch(localEditsSlice.actions.updateCurrentCartesian(event)); + }} + />; +} + +/** + * use a transparent heatmap to capture which point the user clicked on the map + * @param imageDimensions Normalized dimensions of the image + * @param settings Settings for calibration displays + * @returns point and data + */ +function createBackgroundTrace(imageDimensions: Dimensions, settings: CalibrationSettings) { + // define the grid of heatmap + const x: number[] = []; + const y: number[] = []; + // bound the grid to image dimensions to avoid clicking outside of the map + for (let i = 0; i <= Math.ceil(imageDimensions.width); i = i + 1) { + x.push(i); + } + for (let j = 0; j <= Math.ceil(imageDimensions.height); j = j + 1) { + y.push(j); + } + // define the actual points of the graph, numbers in the array are used to designate different colors; + const z: number[][] = []; + for (let ind1 = 0; ind1 < y.length; ++ind1) { + const temp = []; + for (let ind2 = 0; ind2 < x.length; ++ind2) { + temp.push(0); + } + z.push(temp); + } + const trace = { + x, + y, + z, + type: 'heatmap', + colorscale: [['0.5', 'rgba(6,86,157,0)']], // set colors to be fully transparent + xgap: 1, + ygap: 1, + hoverinfo: 'x+y', + opacity: (settings.showGrid) ? '0.5' : '0', // controls whether the grids will be displayed + showscale: false + }; + return trace; +} \ No newline at end of file diff --git a/src/client/app/components/maps/MapCalibrationComponent.tsx b/src/client/app/components/maps/MapCalibrationComponent.tsx index cfdbbd97c..9b71cb86e 100644 --- a/src/client/app/components/maps/MapCalibrationComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationComponent.tsx @@ -9,6 +9,13 @@ import MapCalibrationInitiateContainer from '../../containers/maps/MapCalibratio //import MapsDetailContainer from '../../containers/maps/MapsDetailContainer'; import { CalibrationModeTypes } from '../../types/redux/map'; import MapsDetailComponent from './MapsDetailComponent'; +import { Navigate } from 'react-router-dom'; +import { useAppSelector } from '../../redux/reduxHooks'; +import MapCalibrationInfoDisplayComponent from './MapCalibrationInfoDisplayComponent'; +import MapCalibrationInitiateComponent from './MapCalibrationInitiateComponent'; +import { localEditsSlice } from '../../redux/slices/localEditsSlice'; +import { mapsAdapter } from '../../redux/api/mapsApi'; +import MapCalibrationChartDisplayComponent from './MapCalibrationChartDisplayComponent'; interface MapCalibrationProps { mode: CalibrationModeTypes; @@ -54,3 +61,32 @@ export default class MapCalibrationComponent extends React.Component<MapCalibrat } } } +/** + * @returns Calibration Component corresponding to current step invloved + */ +export const MapCalibrationComponent2 = () => { + const mapToCalibrate = useAppSelector(localEditsSlice.selectors.selectCalibrationMapId); + const calibrationMode = useAppSelector(state => { + const data = mapsAdapter.getSelectors().selectById(state.localEdits.mapEdits, mapToCalibrate); + return data?.calibrationMode ?? CalibrationModeTypes.unavailable; + }); + if (calibrationMode === CalibrationModeTypes.initiate) { + return ( + <div className='container-fluid'> + {/* <MapCalibrationInitiateContainer /> */} + <MapCalibrationInitiateComponent /> + </div > + ); + } else if (calibrationMode === CalibrationModeTypes.calibrate) { + return ( + <div className='container-fluid'> + <div id={'MapCalibrationContainer'}> + <MapCalibrationChartDisplayComponent /> + <MapCalibrationInfoDisplayComponent /> + </div> + </div> + ); + } else { + return <Navigate to='/maps' replace />; + } +}; diff --git a/src/client/app/components/maps/MapCalibrationInfoDisplayComponent.tsx b/src/client/app/components/maps/MapCalibrationInfoDisplayComponent.tsx index 7e27aeede..5e091af28 100644 --- a/src/client/app/components/maps/MapCalibrationInfoDisplayComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationInfoDisplayComponent.tsx @@ -3,116 +3,110 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import {GPSPoint, isValidGPSInput} from '../../utils/calibration'; -import {ChangeEvent, FormEvent} from 'react'; -import {FormattedMessage} from 'react-intl'; +import { ChangeEvent, FormEvent } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { logsApi } from '../../redux/api/logApi'; +import { mapsAdapter, mapsApi } from '../../redux/api/mapsApi'; +import { useTranslate } from '../../redux/componentHooks'; +import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; +import { localEditsSlice } from '../../redux/slices/localEditsSlice'; +import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; -interface InfoDisplayProps { - showGrid: boolean; - currentCartesianDisplay: string; - resultDisplay: string; - changeGridDisplay(): any; - updateGPSCoordinates(gpsCoordinate: GPSPoint): any; - submitCalibratingMap(): any; - dropCurrentCalibration(): any; - log(level: string, message: string): any; -} - -interface InfoDisplayState { - value: string; -} - -export default class MapCalibrationInfoDisplayComponent extends React.Component<InfoDisplayProps, InfoDisplayState> { - constructor(props: InfoDisplayProps) { - super(props); - this.state = { - value: '' - }; - this.handleGridDisplay = this.handleGridDisplay.bind(this); - this.handleGPSInput = this.handleGPSInput.bind(this); - this.resetInputField = this.resetInputField.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleChanges = this.handleChanges.bind(this); - this.dropCurrentCalibration = this.dropCurrentCalibration.bind(this); - } - public render() { - const calibrationDisplay = `${this.props.resultDisplay}`; - return ( - <div> - <div className='checkbox'> - <label><input type='checkbox' onChange={this.handleGridDisplay} checked={this.props.showGrid} /> - <FormattedMessage id='show.grid' /> - </label> - </div> - <div id='UserInput'> - <form onSubmit={this.handleSubmit}> - <label> - <FormattedMessage id='input.gps.coords.first'/> {this.props.currentCartesianDisplay} - <br/> - <FormattedMessage id='input.gps.coords.second'/> - <br/> - <textarea id={'text'} cols={50} value={this.state.value} onChange={this.handleGPSInput}/> - </label> - <br/> - <FormattedMessage id='calibration.submit.button'> - {intlSubmitText => <input type={'submit'} value={intlSubmitText.toString()}/>} - </FormattedMessage> - </form> - <FormattedMessage id='calibration.reset.button'> - {intlResetButton => <button onClick={this.dropCurrentCalibration}>{intlResetButton.toString()}</button>} - </FormattedMessage> - <FormattedMessage id='calibration.save.database'> - {intlSaveChanges => <button onClick={this.handleChanges}>{intlSaveChanges.toString()}</button>} - </FormattedMessage> - <FormattedMessage id='calibration.display'> - {intlResult => <p>{intlResult.toString()}{calibrationDisplay}</p>} - </FormattedMessage> - </div> - </div> - ); - } +/** + * @returns TODO DO ME + */ +export default function MapCalibrationInfoDisplayComponent() { + const dispatch = useAppDispatch(); + const [createNewMap] = mapsApi.useCreateMapMutation(); + const [editMap] = mapsApi.useEditMapMutation(); + const translate = useTranslate(); + const [logToServer] = logsApi.useLogToServerMutation(); + const [value, setValue] = React.useState<string>(''); + const showGrid = useAppSelector(state => state.localEdits.calibrationSettings.showGrid); + const mapData = useAppSelector(state => mapsAdapter.getSelectors().selectById(state.localEdits.mapEdits, state.localEdits.calibratingMap)); + const resultDisplay = (mapData.calibrationResult) + ? `x: ${mapData.calibrationResult.maxError.x}%, y: ${mapData.calibrationResult.maxError.y}%` + : translate('need.more.points'); + const cartesianDisplay = (mapData.currentPoint) + ? `x: ${mapData.currentPoint.cartesian.x}, y: ${mapData.currentPoint.cartesian.y}` + : translate('undefined'); - private handleGridDisplay() { - this.props.changeGridDisplay(); - } + const handleGridDisplay = () => { dispatch(localEditsSlice.actions.toggleMapShowGrid()); }; - private resetInputField() { - this.setState({ - value: '' - }); - } + const resetInputField = () => setValue(''); - private handleSubmit = (event: FormEvent) => { + const handleSubmit = (event: FormEvent) => { event.preventDefault(); const latitudeIndex = 0; const longitudeIndex = 1; - if (this.props.currentCartesianDisplay === 'x: undefined, y: undefined') { return; } - const input = this.state.value; + if (cartesianDisplay === 'x: undefined, y: undefined') { + return; + } + const input = value; if (isValidGPSInput(input)) { const array = input.split(',').map((value: string) => parseFloat(value)); const gps: GPSPoint = { longitude: array[longitudeIndex], latitude: array[latitudeIndex] }; - this.props.updateGPSCoordinates(gps); - this.resetInputField(); + console.log('Verify: this.props.updateGPSCoordinates(gps); ', gps); + + dispatch(localEditsSlice.actions.offerCurrentGPS(gps)); + resetInputField(); } else { - this.props.log('info', `refused data point with invalid input: ${input}`); + logToServer({ level: 'info', message: `refused data point with invalid input: ${input}` }); } }; - private handleGPSInput(event: ChangeEvent<HTMLTextAreaElement>) { - this.setState({ - value: event.target.value - }); - } + const handleGPSInput = (event: ChangeEvent<HTMLTextAreaElement>) => setValue(event.target.value); + + const dropCurrentCalibration = () => { + console.log('Verfiy this.props.dropCurrentCalibration();'); + dispatch(localEditsSlice.actions.resetCalibration(mapData.id)); + }; - private dropCurrentCalibration() { - this.props.dropCurrentCalibration(); - } + const handleChanges = () => { + console.log('Verfiy: // this.props.submitCalibratingMap();'); + if (mapData.id < 0) { + createNewMap(mapData); + } else { + editMap(mapData); + } + }; + const calibrationDisplay = `${resultDisplay}`; + return ( + <div> + <div className='checkbox'> + <label><input type='checkbox' onChange={handleGridDisplay} checked={showGrid} /> + <FormattedMessage id='show.grid' /> + </label> + </div> + <div id='UserInput'> + <form onSubmit={handleSubmit}> + <label> + <FormattedMessage id='input.gps.coords.first' /> {cartesianDisplay} + <br /> + <FormattedMessage id='input.gps.coords.second' /> + <br /> + <textarea id={'text'} cols={50} value={value} onChange={handleGPSInput} /> + </label> + <br /> + <FormattedMessage id='calibration.submit.button'> + {intlSubmitText => <input type={'submit'} value={intlSubmitText.toString()} />} + </FormattedMessage> + </form> + <FormattedMessage id='calibration.reset.button'> + {intlResetButton => <button onClick={dropCurrentCalibration}>{intlResetButton.toString()}</button>} + </FormattedMessage> + <FormattedMessage id='calibration.save.database'> + {intlSaveChanges => <button onClick={handleChanges}>{intlSaveChanges.toString()}</button>} + </FormattedMessage> + <FormattedMessage id='calibration.display'> + {intlResult => <p>{intlResult.toString()}{calibrationDisplay}</p>} + </FormattedMessage> + </div> + </div> + ); - private handleChanges() { - this.props.submitCalibratingMap(); - } } diff --git a/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx b/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx index 1a7cc4d06..dd30dd9f0 100644 --- a/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx @@ -10,6 +10,7 @@ import { logsApi } from '../../redux/api/logApi'; import { selectMapById } from '../../redux/api/mapsApi'; import { useTranslate } from '../../redux/componentHooks'; import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; +import { selectSelectedMap } from '../../redux/slices/graphSlice'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; import { showErrorNotification } from '../../utils/notifications'; @@ -44,12 +45,7 @@ export default function MapCalibrationInitiateComponent() { const [mapName, setMapName] = React.useState<string>(''); const [angle, setAngle] = React.useState<string>(''); const fileRef = React.useRef<HTMLInputElement>(null); - const mapData = useAppSelector(state => selectMapById(state, state.maps.selectedMap)); - // const [mapData] = useAppSelector(state => selectEntityDisplayData(state, { - // type: EntityType.MAP, - // id: state.localEdits.mapCalibration.calibratingMap - // })); - + const mapData = useAppSelector(state => selectMapById(state, selectSelectedMap(state))); const notify = (key: 'map.bad.number' | 'map.bad.digita' | 'map.bad.digitb' | 'map.bad.load' | 'map.bad.name') => { showErrorNotification(translate(key)); diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index 937f65430..e35c321cf 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -7,10 +7,10 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { LocaleDataKey } from 'translations/data'; import { useAppSelector } from '../../redux/reduxHooks'; -import { selectMapById } from '../../redux/selectors/maps'; import '../../styles/card-page.css'; import translate from '../../utils/translate'; import EditMapModalComponent from './EditMapModalComponent'; +import { selectMapById } from '../../redux/api/mapsApi'; interface MapViewProps { mapID: number; } @@ -18,7 +18,7 @@ interface MapViewProps { //TODO: Migrate to RTK const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { - const map = useAppSelector(selectMapById(mapID)); + const map = useAppSelector(state => selectMapById(state, mapID)); // Helper function checks map to see if it's calibrated const getCalibrationStatus = () => { diff --git a/src/client/app/redux/actions/map.ts b/src/client/app/redux/actions/map.ts index 688183839..06882af69 100644 --- a/src/client/app/redux/actions/map.ts +++ b/src/client/app/redux/actions/map.ts @@ -4,17 +4,17 @@ // TODO: Migrate to RTK +import * as moment from 'moment'; import { ActionType, Dispatch, GetState, Thunk } from '../../types/redux/actions'; import * as t from '../../types/redux/map'; import { CalibrationModeTypes, MapData, MapMetadata } from '../../types/redux/map'; -import { calibrate, CalibratedPoint, CalibrationResult, CartesianPoint, Dimensions, GPSPoint } from '../../utils/calibration'; import { State } from '../../types/redux/state'; -import MapsApi from '../../utils/api/MapsApi'; import ApiBackend from '../../utils/api/ApiBackend'; +import MapsApi from '../../utils/api/MapsApi'; +import { calibrate, CalibratedPoint, CalibrationResult, CartesianPoint, GPSPoint } from '../../utils/calibration'; +import { browserHistory } from '../../utils/history'; import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; -import * as moment from 'moment'; -import { browserHistory } from '../../utils/history'; import { logToServer } from './logs'; const mapsApi = new MapsApi(new ApiBackend()); @@ -160,7 +160,7 @@ export function offerCurrentGPS(currentGPS: GPSPoint): Thunk { }; } -function hasCartesian(point: CalibratedPoint) { +export function hasCartesian(point: CalibratedPoint) { return point.cartesian.x !== -1 && point.cartesian.y !== -1; } @@ -189,13 +189,9 @@ function isReadyForCalculation(state: State): boolean { function prepareDataToCalculation(state: State): CalibrationResult { const mapID = state.maps.calibratingMap; const mp = state.maps.editedMaps[mapID]; - const imageDimensions: Dimensions = { - width: mp.imgWidth, - height: mp.imgHeight - }; // Since mp is defined above, calibrationSet is defined. /* eslint-disable @typescript-eslint/no-non-null-assertion */ - const result = calibrate(mp.calibrationSet!, imageDimensions, mp.northAngle); + const result = calibrate(mp); return result; /* eslint-enable @typescript-eslint/no-non-null-assertion */ } diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index 3750dfe3f..9262d4707 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -4,13 +4,10 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { RootState } from '../../store'; -// TODO Should be env variable? -const baseHref = (document.getElementsByTagName('base')[0] || {}).href; export const baseApi = createApi({ reducerPath: 'api', baseQuery: fetchBaseQuery({ - baseUrl: baseHref, prepareHeaders: (headers, { getState }) => { const state = getState() as RootState; // For each api call attempt to set the JWT token in the request header @@ -21,7 +18,6 @@ export const baseApi = createApi({ }, // Default Behavior assumes all responses are json // use content type because API responses are varied - // TODO Validate Behavior against all endpoints responseHandler: 'content-type' }), // The types of tags that any injected endpoint may, provide, or invalidate. diff --git a/src/client/app/redux/api/mapsApi.ts b/src/client/app/redux/api/mapsApi.ts index 9f1afcd21..7a8f7addd 100644 --- a/src/client/app/redux/api/mapsApi.ts +++ b/src/client/app/redux/api/mapsApi.ts @@ -1,6 +1,7 @@ import { createEntityAdapter, EntityState } from '@reduxjs/toolkit'; import { pick } from 'lodash'; import * as moment from 'moment'; +import { createAppSelector } from '../../redux/selectors/selectors'; import { RootState } from '../../store'; import { MapData, MapMetadata } from '../../types/redux/map'; import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; @@ -115,7 +116,6 @@ export const mapsApi = baseApi.injectEndpoints({ showSuccessNotification(translate('updated.map.without.calibration')); } // Cleanup LocalEditsSLice - // TODO Centralize localEditCleanup. Should be same as others. // api.dispatch(localEditsSlice.actions.removeOneEdit({ type: EntityType.MAP, id: map.id })); }).catch(() => { showErrorNotification(translate('failed.to.edit.map')); @@ -145,3 +145,11 @@ export const { selectEntities: selectMapDataById, selectTotal: selectTotalMaps } = mapsAdapter.getSelectors(selectMapApiData); + +export const selectMapSelectOptions = createAppSelector( + [selectAllMaps], + allMaps => allMaps.map(map => ( + { value: map.id, label: map.name, isDisabled: !(map.origin && map.opposite) } + ))); + + diff --git a/src/client/app/redux/rootReducer.ts b/src/client/app/redux/rootReducer.ts index a6d29340c..9e5f33a1c 100644 --- a/src/client/app/redux/rootReducer.ts +++ b/src/client/app/redux/rootReducer.ts @@ -8,14 +8,16 @@ import { adminSlice } from './slices/adminSlice'; import { appStateSlice } from './slices/appStateSlice'; import { currentUserSlice } from './slices/currentUserSlice'; import { graphSlice } from './slices/graphSlice'; -import maps from './reducers/maps'; +// import maps from './reducers/maps'; +import { localEditsSlice } from './slices/localEditsSlice'; export const rootReducer = combineReducers({ appState: appStateSlice.reducer, graph: graphSlice.reducer, admin: adminSlice.reducer, currentUser: currentUserSlice.reducer, + localEdits: localEditsSlice.reducer, // RTK Query's Derived Reducers - [baseApi.reducerPath]: baseApi.reducer, - maps + [baseApi.reducerPath]: baseApi.reducer + // maps }); \ No newline at end of file diff --git a/src/client/app/redux/selectors/chartQuerySelectors.ts b/src/client/app/redux/selectors/chartQuerySelectors.ts index b0253fcc4..7df10b84d 100644 --- a/src/client/app/redux/selectors/chartQuerySelectors.ts +++ b/src/client/app/redux/selectors/chartQuerySelectors.ts @@ -3,17 +3,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { createSelector } from '@reduxjs/toolkit'; -import { RootState } from 'store'; +import { omit } from 'lodash'; import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; import { calculateCompareShift } from '../../utils/calculateCompare'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { selectBarWidthDays, selectComparePeriod, selectCompareTimeInterval, selectMapBarWidthDays, selectQueryTimeInterval, - selectSelectedGroups, selectSelectedMeters, + selectSelectedGroups, selectSelectedMap, selectSelectedMeters, selectSelectedUnit, selectThreeDState } from '../slices/graphSlice'; -import { omit } from 'lodash'; +import { createAppSelector } from './selectors'; // query args that 'most' graphs share export interface commonQueryArgs { @@ -137,11 +137,11 @@ export const selectCompareChartQueryArgs = createSelector( } ); -export const selectMapChartQueryArgs = createSelector( +export const selectMapChartQueryArgs = createAppSelector( selectBarChartQueryArgs, selectMapBarWidthDays, - (state: RootState) => state.maps, - (barChartArgs, barWidthDays, maps) => { + selectSelectedMap, + (barChartArgs, barWidthDays, selectedMap) => { const durationDays = Math.round(barWidthDays.asDays()); const meterArgs: MapReadingApiArgs = { @@ -155,8 +155,8 @@ export const selectMapChartQueryArgs = createSelector( barWidthDays: durationDays }; - const meterShouldSkip = barChartArgs.meterShouldSkip || maps.selectedMap === 0; - const groupShouldSkip = barChartArgs.groupShouldSkip || maps.selectedMap === 0; + const meterShouldSkip = barChartArgs.meterShouldSkip || selectedMap === 0; + const groupShouldSkip = barChartArgs.groupShouldSkip || selectedMap === 0; return { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip }; } diff --git a/src/client/app/redux/selectors/maps.ts b/src/client/app/redux/selectors/maps.ts deleted file mode 100644 index fe068ee73..000000000 --- a/src/client/app/redux/selectors/maps.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// TODO: Migrate to RTK - -import { RootState } from 'store'; -import { MapState } from 'types/redux/map'; -import { createAppSelector } from './selectors'; - -export const selectMapState = (state: RootState) => state.maps; -export const selectMaps = createAppSelector([selectMapState], maps => - Object.keys(maps.byMapID) - .map(key => parseInt(key)) - .filter(key => !isNaN(key)) -); - -export const selectMapById = (id: number) => - createAppSelector([selectMapState], (maps: MapState) => maps.byMapID[id]); diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index b80edd585..26190d805 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -4,6 +4,7 @@ import { sortBy } from 'lodash'; import { selectGroupDataById } from '../../redux/api/groupsApi'; +import { selectMapById } from '../../redux/api/mapsApi'; import { selectMeterDataById } from '../../redux/api/metersApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; import { selectChartLinkHideOptions } from '../../redux/slices/appStateSlice'; @@ -19,10 +20,9 @@ import { } from '../../utils/calibration'; import { metersInGroup, unitsCompatibleWithMeters } from '../../utils/determineCompatibleUnits'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { selectMapState } from '../selectors/maps'; import { selectChartToRender, selectGraphAreaNormalization, selectGraphState, - selectSelectedGroups, selectSelectedMeters, selectSelectedUnit, selectSliderRangeInterval + selectSelectedGroups, selectSelectedMap, selectSelectedMeters, selectSelectedUnit, selectSliderRangeInterval } from '../slices/graphSlice'; import { selectVisibleMetersAndGroups, selectVisibleUnitOrSuffixState } from './authVisibilitySelectors'; import { selectDefaultGraphicUnitFromEntity, selectMeterOrGroupFromEntity, selectNameFromEntity } from './entitySelectors'; @@ -131,11 +131,12 @@ export const selectChartTypeCompatibility = createAppSelector( [ selectCurrentAreaCompatibility, selectChartToRender, + selectSelectedMap, selectMeterDataById, selectGroupDataById, - selectMapState + state => selectMapById(state, selectSelectedMap(state)) ], - (areaCompat, chartToRender, meterDataById, groupDataById, mapState) => { + (areaCompat, chartToRender, selectedMap, meterDataById, groupDataById, selectedMapMetadata) => { // Deep Copy previous selector's values, and update as needed based on current ChartType(s) const compatibleMeters = new Set<number>(Array.from(areaCompat.compatibleMeters)); const incompatibleMeters = new Set<number>(Array.from(areaCompat.incompatibleMeters)); @@ -144,13 +145,12 @@ export const selectChartTypeCompatibility = createAppSelector( const incompatibleGroups = new Set<number>(Array.from(areaCompat.incompatibleGroups)); // ony run this check if we are displaying a map chart - if (chartToRender === ChartTypes.map && mapState.selectedMap !== 0) { - const mp = mapState.byMapID[mapState.selectedMap]; + if (chartToRender === ChartTypes.map && selectedMap !== 0) { // filter meters; // The size of the original map loaded into OED. const imageDimensions: Dimensions = { - width: mp.imgWidth, - height: mp.imgHeight + width: selectedMapMetadata.imgWidth, + height: selectedMapMetadata.imgHeight }; // Determine the dimensions so within the Plotly coordinates on the user map. const imageDimensionNormalized = normalizeImageDimensions(imageDimensions); @@ -167,19 +167,19 @@ export const selectChartTypeCompatibility = createAppSelector( // and upper, right corners of the user map. // The gps value can be null from the database. Note using gps !== null to check for both null and undefined // causes TS to complain about the unknown case so not used. - const origin = mp.origin; - const opposite = mp.opposite; + const origin = selectedMapMetadata.origin; + const opposite = selectedMapMetadata.opposite; compatibleMeters.forEach(meterID => { // This meter's GPS value. const gps = meterDataById[meterID].gps; - if (origin !== undefined && opposite !== undefined && gps !== undefined && gps !== null) { + if (origin && opposite && gps) { // Get the GPS degrees per unit of Plotly grid for x and y. By knowing the two corners // (or really any two distinct points) you can calculate this by the change in GPS over the // change in x or y which is the map's width & height in this case. - const scaleOfMap = calculateScaleFromEndpoints(origin, opposite, imageDimensionNormalized, mp.northAngle); + const scaleOfMap = calculateScaleFromEndpoints(origin, opposite, imageDimensionNormalized, selectedMapMetadata.northAngle); // Convert GPS of meter to grid on user map. See calibration.ts for more info on this. - const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, mp.northAngle); - if (!(itemMapInfoOk(meterID, DataType.Meter, mp, gps) && + const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, selectedMapMetadata.northAngle); + if (!(itemMapInfoOk(meterID, DataType.Meter, selectedMapMetadata, gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid))) { incompatibleMeters.add(meterID); } @@ -192,10 +192,10 @@ export const selectChartTypeCompatibility = createAppSelector( // The below code follows the logic for meters shown above. See comments above for clarification on the below code. compatibleGroups.forEach(groupID => { const gps = groupDataById[groupID].gps; - if (origin !== undefined && opposite !== undefined && gps !== undefined && gps !== null) { - const scaleOfMap = calculateScaleFromEndpoints(origin, opposite, imageDimensionNormalized, mp.northAngle); - const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, mp.northAngle); - if (!(itemMapInfoOk(groupID, DataType.Group, mp, gps) && + if (origin && opposite && gps) { + const scaleOfMap = calculateScaleFromEndpoints(origin, opposite, imageDimensionNormalized, selectedMapMetadata.northAngle); + const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, selectedMapMetadata.northAngle); + if (!(itemMapInfoOk(groupID, DataType.Group, selectedMapMetadata, gps) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid))) { incompatibleGroups.add(groupID); } @@ -435,7 +435,7 @@ export const selectChartLink = createAppSelector( selectGraphState, selectChartLinkHideOptions, selectSliderRangeInterval, - state => state.maps.selectedMap + selectSelectedMap ], (current, chartLinkHideOptions, rangeSliderInterval, selectedMap) => { // Determine the beginning of the URL to add arguments to. diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index 1114225e4..a5ea57bd1 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -17,12 +17,15 @@ import { ChartTypes, GraphState, LineGraphRate, MeterOrGroup, ReadingInterval } import { ComparePeriod, SortingOrder, calculateCompareTimeInterval, validateComparePeriod, validateSortingOrder } from '../../utils/calculateCompare'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import { preferencesApi } from '../api/preferencesApi'; +import { mapsApi } from '../../redux/api/mapsApi'; const defaultState: GraphState = { selectedMeters: [], selectedGroups: [], selectedUnit: -99, selectedAreaUnit: AreaUnitType.none, + // TODO appropriate default value? + selectedMap: 0, queryTimeInterval: TimeInterval.unbounded(), rangeSliderInterval: TimeInterval.unbounded(), barDuration: moment.duration(4, 'weeks'), @@ -58,6 +61,9 @@ export const graphSlice = createSlice({ name: 'graph', initialState: initialState, reducers: { + updateSelectedMaps: (state, action: PayloadAction<number>) => { + state.current.selectedMap = action.payload; + }, updateSelectedMeters: (state, action: PayloadAction<number[]>) => { state.current.selectedMeters = action.payload; }, @@ -355,6 +361,16 @@ export const graphSlice = createSlice({ }); } ) + .addMatcher( + mapsApi.endpoints.getMapDetails.matchFulfilled, + ({ current }, action) => { + // On Fetch fulfilled + // If there is only one map, selectedMap is the id of the only map. ie; display map automatically if only 1 map + if (!current.hotlinked && action.payload.ids.length === 1) { + current.selectedMap = action.payload.ids[0]; + } + } + ) .addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, ({ current }, action) => { if (!current.hotlinked) { const { defaultAreaUnit, defaultChartToRender, defaultBarStacking, defaultAreaNormalization } = action.payload; @@ -374,6 +390,7 @@ export const graphSlice = createSlice({ selectBarStacking: state => state.current.barStacking, selectBarWidthDays: state => state.current.barDuration, selectMapBarWidthDays: state => state.current.mapsBarDuration, + selectSelectedMap: state => state.current.selectedMap, selectAreaUnit: state => state.current.selectedAreaUnit, selectSelectedUnit: state => state.current.selectedUnit, selectChartToRender: state => state.current.chartToRender, @@ -411,7 +428,7 @@ export const { selectGraphAreaNormalization, selectSliderRangeInterval, selectDefaultGraphState, selectHistoryIsDirty, selectPlotlySliderMax, selectPlotlySliderMin, - selectMapBarWidthDays + selectMapBarWidthDays, selectSelectedMap } = graphSlice.selectors; // actionCreators exports @@ -428,6 +445,7 @@ export const { toggleAreaNormalization, updateThreeDMeterOrGroup, changeCompareSortingOrder, updateThreeDMeterOrGroupID, updateThreeDReadingInterval, updateThreeDMeterOrGroupInfo, - updateSelectedMetersOrGroups, updateMapsBarDuration + updateSelectedMetersOrGroups, updateMapsBarDuration, + updateSelectedMaps } = graphSlice.actions; diff --git a/src/client/app/redux/slices/localEditsSlice.ts b/src/client/app/redux/slices/localEditsSlice.ts new file mode 100644 index 000000000..7845a1811 --- /dev/null +++ b/src/client/app/redux/slices/localEditsSlice.ts @@ -0,0 +1,122 @@ +import { createEntityAdapter } from '@reduxjs/toolkit'; +import { hasCartesian } from '../../redux/actions/map'; +import { createThunkSlice } from '../../redux/sliceCreators'; +import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; +import { calibrate, CalibratedPoint, CartesianPoint, GPSPoint } from '../../utils/calibration'; +import { mapsAdapter } from '../../redux/api/mapsApi'; +import { PlotMouseEvent } from 'plotly.js'; + +const localEditAdapter = createEntityAdapter<MapMetadata>(); +export const localEditsSlice = createThunkSlice({ + name: 'localEdits', + initialState: { + // Maps + mapEdits: mapsAdapter.getInitialState(), + calibratingMap: 0, + newMapIdCounter: 0, + calibrationSettings: { + calibrationThreshold: 3, + showGrid: false + + } + + }, + reducers: create => ({ + incrementCounter: create.reducer<void>(state => { + state.newMapIdCounter++; + }), + toggleMapShowGrid: create.reducer<void>(state => { + state.calibrationSettings.showGrid; + }), + createNewMap: create.reducer(state => { + state.newMapIdCounter++; + const temporaryID = state.newMapIdCounter * -1; + localEditAdapter.setOne(state.mapEdits, { ...emptyMetadata, id: temporaryID }); + state.calibratingMap = temporaryID; + }), + offerCurrentGPS: create.reducer<GPSPoint>((state, { payload }) => { + // Stripped offerCurrentGPS thunk into a single reducer for simplicity. The only missing functionality are the serverlogs + // Current axios approach doesn't require dispatch, however if moved to rtk will. thunks for this adds complexity + // For simplicity, these logs can instead be tabulated in a middleware.(probably.) + const map = localEditAdapter.getSelectors().selectById(state.mapEdits, state.calibratingMap); + const point = map.currentPoint; + if (point && hasCartesian(point)) { + point.gps = payload; + map.calibrationSet.push(point); + if (map.calibrationSet.length >= state.calibrationSettings.calibrationThreshold) { + // Since mp is defined above, calibrationSet is defined. + const result = calibrate(map); + map.calibrationResult = result; + } + } + }), + updateCurrentCartesian: create.reducer<PlotMouseEvent>((state, { payload }) => { + // repourposed getClickedCoordinate Events from previous maps implementatinon moved to reducer + // trace 0 keeps a transparent trace of closely positioned points used for calibration(backgroundTrace), + // trace 1 keeps the data points used for calibration are automatically added to the same trace(dataPointTrace), + // event.points will include all points near a mouse click, including those in the backgroundTrace and the dataPointTrace, + // so the algorithm only looks at trace 0 since points from trace 1 are already put into the data set used for calibration. + const eligiblePoints = []; + for (const point of payload.points) { + const traceNumber = point.curveNumber; + if (traceNumber === 0) { + eligiblePoints.push(point); + } + } + const xValue = eligiblePoints[0].x as number; + const yValue = eligiblePoints[0].y as number; + const clickedPoint: CartesianPoint = { + x: Number(xValue.toFixed(6)), + y: Number(yValue.toFixed(6)) + }; + + // update calibrating map with new datapoint + const currentPoint: CalibratedPoint = { + cartesian: clickedPoint, + gps: { longitude: -1, latitude: -1 } + }; + + localEditAdapter.updateOne(state.mapEdits, { + id: state.calibratingMap, + changes: { currentPoint } + }); + + + }), + resetCalibration: create.reducer<number>((state, { payload }) => { + localEditAdapter.updateOne(state.mapEdits, { + id: payload, + changes: { + currentPoint: undefined, + calibrationResult: undefined, + calibrationSet: [] + } + }); + }) + }), + + selectors: { + selectCalibrationMapId: state => state.calibratingMap + } +}); + +// MAP Stuff TODO RELOCATE +const emptyMetadata: MapMetadata = { + id: 0, + name: '', + displayable: false, + note: undefined, + filename: '', + modifiedDate: '', + origin: undefined, + opposite: undefined, + mapSource: '', + imgHeight: 0, + imgWidth: 0, + calibrationMode: CalibrationModeTypes.initiate, + currentPoint: undefined, + calibrationSet: [], + calibrationResult: undefined, + northAngle: 0, + circleSize: 0 +}; diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index e649dd143..8640761ac 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -60,6 +60,7 @@ export interface GraphState { selectedMeters: number[]; selectedGroups: number[]; selectedUnit: number; + selectedMap: number; selectedAreaUnit: AreaUnitType; rangeSliderInterval: TimeInterval; barDuration: moment.Duration; diff --git a/src/client/app/types/redux/state.ts b/src/client/app/types/redux/state.ts deleted file mode 100644 index 1d97657ef..000000000 --- a/src/client/app/types/redux/state.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { BarReadingsState } from './barReadings'; -import { LineReadingsState } from './lineReadings'; -import { GraphState } from './graph'; -import { GroupsState } from './groups'; -import { MetersState } from './meters'; -import { AdminState } from './admin'; -import { CompareReadingsState } from './compareReadings'; -import { VersionState } from './version'; -import { MapState } from './map'; -import { CurrentUserState } from './currentUser'; -import { UnsavedWarningState } from './unsavedWarning'; -import { UnitsState } from './units'; -import { ConversionsState } from './conversions'; -import { AppState } from 'redux/slices/appStateSlice'; - -export interface State { - appState: AppState; - meters: MetersState; - readings: { - line: LineReadingsState; - bar: BarReadingsState; - compare: CompareReadingsState; - }; - graph: GraphState; - maps: MapState; - groups: GroupsState; - admin: AdminState; - version: VersionState; - currentUser: CurrentUserState; - unsavedWarning: UnsavedWarningState; - units: UnitsState; - conversions: ConversionsState; -} diff --git a/src/client/app/utils/calibration.ts b/src/client/app/utils/calibration.ts index 7ec057a4b..63e2ca8a5 100644 --- a/src/client/app/utils/calibration.ts +++ b/src/client/app/utils/calibration.ts @@ -177,12 +177,19 @@ export function gpsToUserGrid(size: Dimensions, gps: GPSPoint, originGPS: GPSPoi * origin and opposite points. It also calculates the relative error for this * scale by finding the maximum difference between the calculated scale and the * one from each pair of calibration points. It returns all three of these values. - * @param calibrationSet All the points clicked by the user for calibration. - * @param imageDimensions The dimensions of the original map to use from the user. - * @param northAngle The angle between true north and straight up on the map image. + * @param map TODO + * calibrationSet All the points clicked by the user for calibration. + * imageDimensions The dimensions of the original map to use from the user. + * northAngle The angle between true north and straight up on the map image. * @returns The error and the origin & opposite point in GPS to use for mapping. */ -export function calibrate(calibrationSet: CalibratedPoint[], imageDimensions: Dimensions, northAngle: number): CalibrationResult { +export function calibrate(map: MapMetadata): CalibrationResult { + const { calibrationSet, northAngle } = map; + const imageDimensions: Dimensions = { + width: map.imgWidth, + height: map.imgHeight + }; + // calibrationSet: CalibratedPoint[], imageDimensions: Dimensions, northAngle: number // Normalize dimensions to grid used in Plotly const normalizedDimensions = normalizeImageDimensions(imageDimensions); // Array to hold the map scale for each pair of points. From 17c851e7efcdff0414aec819aed73986c460be27 Mon Sep 17 00:00:00 2001 From: ChrisMart21 <ChrisMart21@github.com> Date: Sun, 11 Aug 2024 13:55:04 -0700 Subject: [PATCH 39/50] Comments --- src/client/app/redux/slices/appStateSlice.ts | 8 ++++---- src/client/app/redux/slices/currentUserSlice.ts | 3 ++- src/client/app/redux/slices/graphSlice.ts | 4 ++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/client/app/redux/slices/appStateSlice.ts b/src/client/app/redux/slices/appStateSlice.ts index 8099e5def..a96db219b 100644 --- a/src/client/app/redux/slices/appStateSlice.ts +++ b/src/client/app/redux/slices/appStateSlice.ts @@ -6,8 +6,7 @@ import * as moment from 'moment'; import { processGraphLink } from '../../redux/actions/extraActions'; import { mapsApi } from '../../redux/api/mapsApi'; import { LanguageTypes } from '../../types/redux/i18n'; -import { deleteToken, getToken, hasToken } from '../../utils/token'; -import { fetchMapsDetails } from '../actions/map'; +import { getToken, hasToken } from '../../utils/token'; import { authApi } from '../api/authApi'; import { conversionsApi } from '../api/conversionsApi'; import { groupsApi } from '../api/groupsApi'; @@ -61,6 +60,7 @@ export const appStateSlice = createThunkSlice({ async (_: void, { dispatch }) => { // These queries will trigger a api request, and add a subscription to the store. // Typically they return an unsubscribe method, however we always want to be subscribed to any cache changes for these endpoints. + // Unlike QueryHooks used in other components, these Queries will remain indefinitely subscribed; dispatch(preferencesApi.endpoints.getPreferences.initiate()); dispatch(versionApi.endpoints.getVersion.initiate()); dispatch(unitsApi.endpoints.getUnitsDetails.initiate()); @@ -68,7 +68,7 @@ export const appStateSlice = createThunkSlice({ dispatch(conversionsApi.endpoints.getCikDetails.initiate()); // Older style thunk fetch cycle for maps until migration - dispatch(fetchMapsDetails()); + // dispatch(fetchMapsDetails()); // If user is an admin, they receive additional meter details. // To avoid sending duplicate requests upon startup, verify user then fetch @@ -89,7 +89,6 @@ export const appStateSlice = createThunkSlice({ } catch { // User had a token that isn't valid or getUserDetails threw an error. // Assume token is invalid. Delete if any - deleteToken(); dispatch(currentUserSlice.actions.clearCurrentUser()); } @@ -101,6 +100,7 @@ export const appStateSlice = createThunkSlice({ }, { + // Callback triggers when thunk completes, on either success OR failure. settled: state => { state.initComplete = true; } diff --git a/src/client/app/redux/slices/currentUserSlice.ts b/src/client/app/redux/slices/currentUserSlice.ts index 3bb734a4c..85d655a41 100644 --- a/src/client/app/redux/slices/currentUserSlice.ts +++ b/src/client/app/redux/slices/currentUserSlice.ts @@ -6,7 +6,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import { UserRole } from '../../types/items'; import { CurrentUserState } from '../../types/redux/currentUser'; -import { setToken } from '../../utils/token'; +import { deleteToken, setToken } from '../../utils/token'; import { authApi } from '../api/authApi'; import { userApi } from '../api/userApi'; @@ -25,6 +25,7 @@ export const currentUserSlice = createSlice({ clearCurrentUser: state => { state.profile = null; state.token = null; + deleteToken(); }, setUserToken: (state, action: PayloadAction<string | null>) => { state.token = action.payload; diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index a5ea57bd1..8f83cacc2 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -60,6 +60,8 @@ const initialState: History<GraphState> = { export const graphSlice = createSlice({ name: 'graph', initialState: initialState, + // Current History Implementation tracks ANY action defined in 'reducers' using IsAnyOf(...graphslice.actions) + // To update the current graphState without causing a history entry to be created, utilize the 'Extra Reducers' property reducers: { updateSelectedMaps: (state, action: PayloadAction<number>) => { state.current.selectedMap = action.payload; @@ -247,6 +249,8 @@ export const graphSlice = createSlice({ }, extraReducers: builder => { + // Current History Implementation tracks ANY action defined in 'reducers' + // To update graphState without causing a history entry to be created, utilize the 'Extra Reducers' property builder .addCase( updateHistory, From 5c8dbc16c1438fb5754c5f4cb1b4caa4a3d9e660 Mon Sep 17 00:00:00 2001 From: ChrisMart21 <ChrisMart21@github.com> Date: Mon, 12 Aug 2024 17:03:16 -0700 Subject: [PATCH 40/50] More Maps Changes --- src/client/app/components/RouteComponent.tsx | 4 +- .../conversion/ConversionsDetailComponent.tsx | 3 +- .../components/maps/EditMapModalComponent.tsx | 95 +++-- .../MapCalibrationChartDisplayComponent.tsx | 4 +- .../maps/MapCalibrationComponent.tsx | 61 +-- .../MapCalibrationInfoDisplayComponent.tsx | 7 +- .../maps/MapCalibrationInitiateComponent.tsx | 8 +- .../app/components/maps/MapViewComponent.tsx | 28 +- .../components/maps/MapsDetailComponent.tsx | 9 +- .../app/containers/MapChartContainer.ts | 353 ------------------ .../MapCalibrationChartDisplayContainer.ts | 173 --------- .../maps/MapCalibrationContainer.ts | 19 - .../MapCalibrationInfoDisplayContainer.ts | 38 -- .../maps/MapCalibrationInitiateContainer.ts | 25 -- .../app/containers/maps/MapViewContainer.tsx | 32 -- .../containers/maps/MapsDetailContainer.tsx | 28 -- src/client/app/redux/actions/map.ts | 22 +- src/client/app/redux/api/groupsApi.ts | 8 +- src/client/app/redux/api/mapsApi.ts | 31 +- src/client/app/redux/api/metersApi.ts | 8 +- src/client/app/redux/api/unitsApi.ts | 7 +- src/client/app/redux/devToolConfig.ts | 3 +- src/client/app/redux/entityAdapters.ts | 45 +++ src/client/app/redux/slices/appStateSlice.ts | 8 +- .../app/redux/slices/localEditsSlice.ts | 42 ++- src/client/app/types/redux/actions.ts | 8 +- 26 files changed, 209 insertions(+), 860 deletions(-) delete mode 100644 src/client/app/containers/MapChartContainer.ts delete mode 100644 src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts delete mode 100644 src/client/app/containers/maps/MapCalibrationContainer.ts delete mode 100644 src/client/app/containers/maps/MapCalibrationInfoDisplayContainer.ts delete mode 100644 src/client/app/containers/maps/MapCalibrationInitiateContainer.ts delete mode 100644 src/client/app/containers/maps/MapViewContainer.tsx delete mode 100644 src/client/app/containers/maps/MapsDetailContainer.tsx create mode 100644 src/client/app/redux/entityAdapters.ts diff --git a/src/client/app/components/RouteComponent.tsx b/src/client/app/components/RouteComponent.tsx index 827645741..e2cb41e8a 100644 --- a/src/client/app/components/RouteComponent.tsx +++ b/src/client/app/components/RouteComponent.tsx @@ -16,7 +16,7 @@ import AdminComponent from './admin/AdminComponent'; import UsersDetailComponent from './admin/users/UsersDetailComponent'; import ConversionsDetailComponent from './conversion/ConversionsDetailComponent'; import GroupsDetailComponent from './groups/GroupsDetailComponent'; -import { MapCalibrationComponent2 } from './maps/MapCalibrationComponent'; +import { MapCalibrationComponent } from './maps/MapCalibrationComponent'; import MapsDetailComponent from './maps/MapsDetailComponent'; import MetersDetailComponent from './meters/MetersDetailComponent'; import AdminOutlet from './router/AdminOutlet'; @@ -54,7 +54,7 @@ const router = createBrowserRouter([ element: <AdminOutlet />, children: [ { path: 'admin', element: <AdminComponent /> }, - { path: 'calibration', element: <MapCalibrationComponent2 /> }, + { path: 'calibration', element: <MapCalibrationComponent /> }, { path: 'maps', element: <MapsDetailComponent /> }, { path: 'units', element: <UnitsDetailComponent /> }, { path: 'conversions', element: <ConversionsDetailComponent /> }, diff --git a/src/client/app/components/conversion/ConversionsDetailComponent.tsx b/src/client/app/components/conversion/ConversionsDetailComponent.tsx index 41654ed53..fc4a48528 100644 --- a/src/client/app/components/conversion/ConversionsDetailComponent.tsx +++ b/src/client/app/components/conversion/ConversionsDetailComponent.tsx @@ -7,11 +7,12 @@ import { FormattedMessage } from 'react-intl'; import SpinnerComponent from '../SpinnerComponent'; import TooltipHelpComponent from '../TooltipHelpComponent'; import { conversionsApi, stableEmptyConversions } from '../../redux/api/conversionsApi'; -import { stableEmptyUnitDataById, unitsAdapter, unitsApi } from '../../redux/api/unitsApi'; +import { stableEmptyUnitDataById, unitsApi } from '../../redux/api/unitsApi'; import { ConversionData } from '../../types/redux/conversions'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import ConversionViewComponent from './ConversionViewComponent'; import CreateConversionModalComponent from './CreateConversionModalComponent'; +import { unitsAdapter } from '../../redux/entityAdapters'; /** * Defines the conversions page card view diff --git a/src/client/app/components/maps/EditMapModalComponent.tsx b/src/client/app/components/maps/EditMapModalComponent.tsx index 588f4bfd3..f02cb2656 100644 --- a/src/client/app/components/maps/EditMapModalComponent.tsx +++ b/src/client/app/components/maps/EditMapModalComponent.tsx @@ -2,15 +2,17 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { debounce, isEqual } from 'lodash'; import * as React from 'react'; import { useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; -import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input } from 'reactstrap'; +import { Link } from 'react-router-dom'; +import { Button, Form, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; +import { mapsApi, selectMapById } from '../../redux/api/mapsApi'; +import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; +import { localEditsSlice } from '../../redux/slices/localEditsSlice'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; -import { editMapDetails, submitEditedMap, removeMap, setCalibration } from '../../redux/actions/map'; import { showErrorNotification } from '../../utils/notifications'; -import { useAppDispatch } from '../../redux/reduxHooks'; -import { AppDispatch } from 'store'; interface EditMapModalProps { map: MapMetadata; @@ -19,47 +21,65 @@ interface EditMapModalProps { // TODO: Migrate to RTK const EditMapModalComponent: React.FC<EditMapModalProps> = ({ map }) => { const [showModal, setShowModal] = useState(false); - const handleShow = () => setShowModal(true); - const handleClose = () => setShowModal(false); - const dispatch: AppDispatch = useAppDispatch(); + const dispatch = useAppDispatch(); const [nameInput, setNameInput] = useState(map.name); const [noteInput, setNoteInput] = useState(map.note || ''); - const [circleInput, setCircleInput] = useState(map.circleSize.toString()); + const [circleInput, setCircleInput] = useState(map.circleSize); const [displayable, setDisplayable] = useState(map.displayable); - + const [submitEdit] = mapsApi.useEditMapMutation(); + const [deleteMap] = mapsApi.useDeleteMapMutation(); + // Only used to track stable reference changes to reset form. + const apiMapCache = useAppSelector(state => selectMapById(state, map.id)); const intl = useIntl(); + + const handleShow = () => setShowModal(true); + const handleClose = () => setShowModal(false); + const updatedMap = (): MapMetadata => ({ + ...map, + name: nameInput, + note: noteInput, + circleSize: circleInput, + displayable: displayable + }); + const debouncedLocalUpdate = React.useMemo(() => debounce( + (map: MapMetadata) => !isEqual(map, updatedMap()) && dispatch(localEditsSlice.actions.setOneEdit(map)), + 1000 + ), []); + React.useEffect(() => { debouncedLocalUpdate(updatedMap()); }, [nameInput, noteInput, circleInput, displayable]); + + // Sync with API Cache changes, if any. + React.useEffect(() => { + setNameInput(map.name); + setNoteInput(map.note || ''); + setCircleInput(map.circleSize); + setDisplayable(map.displayable); + }, [apiMapCache]); + const handleSave = () => { - const updatedMap = { - ...map, - name: nameInput, - note: noteInput, - circleSize: parseFloat(circleInput), - displayable: displayable - }; - dispatch(editMapDetails(updatedMap)); - dispatch(submitEditedMap(updatedMap.id)); + submitEdit(updatedMap()); handleClose(); }; const handleDelete = () => { const consent = window.confirm(intl.formatMessage({ id: 'map.confirm.remove' }, { name: map.name })); if (consent) { - dispatch(removeMap(map.id)); + deleteMap(map.id); handleClose(); } }; const handleCalibrationSetting = (mode: CalibrationModeTypes) => { - dispatch(setCalibration(mode, map.id)); + // Add/update entry to localEdits Slice + dispatch(localEditsSlice.actions.setOneEdit(updatedMap())); + // Update Calibration Mode + dispatch(localEditsSlice.actions.updateMapCalibrationMode({ mode, id: map.id })); handleClose(); }; + const circIsValid = circleInput > 0.0 && circleInput <= 2.0; const toggleCircleEdit = () => { - const regtest = /^\d+(\.\d+)?$/; - if (regtest.test(circleInput) && parseFloat(circleInput) <= 2.0) { - setCircleInput(circleInput); - } else { + if (!circIsValid) { showErrorNotification(intl.formatMessage({ id: 'invalid.number' })); } }; @@ -102,9 +122,10 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ map }) => { <Input id="mapCircleSize" type='number' - value={circleInput} - onChange={e => setCircleInput(e.target.value)} - invalid={parseFloat(circleInput) < 0} + value={String(circleInput)} + onChange={e => setCircleInput(parseFloat(e.target.value))} + invalid={!circIsValid} + step={0.1} onBlur={toggleCircleEdit} /> </FormGroup> @@ -114,7 +135,7 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ map }) => { id="mapNote" type="textarea" value={noteInput} - onChange={e => setNoteInput(e.target.value.slice(0, 30))} + onChange={e => setNoteInput(e.target.value)} /> </FormGroup> </Form> @@ -127,18 +148,22 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ map }) => { defaultValue={map.filename} disabled> </Input> - <Button color='primary' onClick={() => handleCalibrationSetting(CalibrationModeTypes.initiate)}> - <FormattedMessage id='map.upload.new.file' /> - </Button> + <Link to='/calibration' onClick={() => handleCalibrationSetting(CalibrationModeTypes.initiate)}> + <Button color='primary' > + <FormattedMessage id='map.upload.new.file' /> + </Button> + </Link> </div> <div> <Label><FormattedMessage id="map.calibration" /></Label> <p> <FormattedMessage id={map.origin && map.opposite ? 'map.is.calibrated' : 'map.is.not.calibrated'} /> </p> - <Button color='primary' onClick={() => handleCalibrationSetting(CalibrationModeTypes.calibrate)}> - <FormattedMessage id='map.calibrate' /> - </Button> + <Link to='/calibration' onClick={() => handleCalibrationSetting(CalibrationModeTypes.calibrate)}> + <Button color='primary' > + <FormattedMessage id='map.calibrate' /> + </Button> + </Link> </div> </ModalBody> <ModalFooter> @@ -148,7 +173,7 @@ const EditMapModalComponent: React.FC<EditMapModalProps> = ({ map }) => { <Button color="secondary" onClick={handleClose}> <FormattedMessage id="cancel" /> </Button> - <Button color="primary" onClick={handleSave}> + <Button color="primary" onClick={handleSave} disabled={!circIsValid}> <FormattedMessage id="save.all" /> </Button> </ModalFooter> diff --git a/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx b/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx index a2f0e071a..fa415b16d 100644 --- a/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx @@ -11,7 +11,7 @@ import { localEditsSlice } from '../../redux/slices/localEditsSlice'; import Locales from '../../types/locales'; import { CalibrationSettings } from '../../types/redux/map'; import { Dimensions, normalizeImageDimensions } from '../../utils/calibration'; -import { mapsAdapter } from '../../redux/api/mapsApi'; +import { selectMapById } from '../../redux/api/mapsApi'; /** * @returns TODO DO ME @@ -22,7 +22,7 @@ export default function MapCalibrationChartDisplayContainer() { const y: number[] = []; const texts: string[] = []; const currentLanguange = useAppSelector(selectSelectedLanguage); - const map = useAppSelector(state => mapsAdapter.getSelectors().selectById(state.localEdits.mapEdits, state.localEdits.calibratingMap)); + const map = useAppSelector(state => selectMapById(state, state.localEdits.calibratingMap)); const settings = useAppSelector(state => state.localEdits.calibrationSettings); const points = map.calibrationSet; diff --git a/src/client/app/components/maps/MapCalibrationComponent.tsx b/src/client/app/components/maps/MapCalibrationComponent.tsx index 9b71cb86e..17c6adf6a 100644 --- a/src/client/app/components/maps/MapCalibrationComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationComponent.tsx @@ -3,71 +3,22 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import MapCalibrationChartDisplayContainer from '../../containers/maps/MapCalibrationChartDisplayContainer'; -import MapCalibrationInfoDisplayContainer from '../../containers/maps/MapCalibrationInfoDisplayContainer'; -import MapCalibrationInitiateContainer from '../../containers/maps/MapCalibrationInitiateContainer'; -//import MapsDetailContainer from '../../containers/maps/MapsDetailContainer'; -import { CalibrationModeTypes } from '../../types/redux/map'; -import MapsDetailComponent from './MapsDetailComponent'; import { Navigate } from 'react-router-dom'; import { useAppSelector } from '../../redux/reduxHooks'; -import MapCalibrationInfoDisplayComponent from './MapCalibrationInfoDisplayComponent'; -import MapCalibrationInitiateComponent from './MapCalibrationInitiateComponent'; import { localEditsSlice } from '../../redux/slices/localEditsSlice'; -import { mapsAdapter } from '../../redux/api/mapsApi'; +import { CalibrationModeTypes } from '../../types/redux/map'; import MapCalibrationChartDisplayComponent from './MapCalibrationChartDisplayComponent'; +import MapCalibrationInfoDisplayComponent from './MapCalibrationInfoDisplayComponent'; +import MapCalibrationInitiateComponent from './MapCalibrationInitiateComponent'; +import { selectMapById } from '../../redux/api/mapsApi'; -interface MapCalibrationProps { - mode: CalibrationModeTypes; - isLoading: boolean; - mapID: number; -} - -export default class MapCalibrationComponent extends React.Component<MapCalibrationProps> { - constructor(props: any) { - super(props); - } - - public render() { - if (this.props.mode === CalibrationModeTypes.initiate) { - return ( - <div className='container-fluid'> - {/* <UnsavedWarningContainer /> */} - <MapCalibrationInitiateContainer /> - </div> - ); - } else if (this.props.mode === CalibrationModeTypes.calibrate) { - return ( - <div className='container-fluid'> - {/* <UnsavedWarningContainer /> */} - <div id={'MapCalibrationContainer'}> - {/* TODO These types of plotly containers expect a lot of passed - values and it gives a TS error. Given we plan to replace this - with the react hooks version and it does not seem to cause any - issues, this TS error is being suppressed for now. - eslint-disable-next-line @typescript-eslint/ban-ts-comment - @ts-ignore */} - <MapCalibrationChartDisplayContainer /> - <MapCalibrationInfoDisplayContainer /> - </div> - </div> - ); - } else { // preview mode containers - return ( - <div className='container-fluid'> - <MapsDetailComponent /> - </div> - ); - } - } -} /** * @returns Calibration Component corresponding to current step invloved */ -export const MapCalibrationComponent2 = () => { +export const MapCalibrationComponent = () => { const mapToCalibrate = useAppSelector(localEditsSlice.selectors.selectCalibrationMapId); const calibrationMode = useAppSelector(state => { - const data = mapsAdapter.getSelectors().selectById(state.localEdits.mapEdits, mapToCalibrate); + const data = selectMapById(state, mapToCalibrate); return data?.calibrationMode ?? CalibrationModeTypes.unavailable; }); if (calibrationMode === CalibrationModeTypes.initiate) { diff --git a/src/client/app/components/maps/MapCalibrationInfoDisplayComponent.tsx b/src/client/app/components/maps/MapCalibrationInfoDisplayComponent.tsx index 5e091af28..359d96459 100644 --- a/src/client/app/components/maps/MapCalibrationInfoDisplayComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationInfoDisplayComponent.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { ChangeEvent, FormEvent } from 'react'; import { FormattedMessage } from 'react-intl'; import { logsApi } from '../../redux/api/logApi'; -import { mapsAdapter, mapsApi } from '../../redux/api/mapsApi'; +import { mapsApi, selectMapById } from '../../redux/api/mapsApi'; import { useTranslate } from '../../redux/componentHooks'; import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; import { localEditsSlice } from '../../redux/slices/localEditsSlice'; @@ -23,7 +23,7 @@ export default function MapCalibrationInfoDisplayComponent() { const [logToServer] = logsApi.useLogToServerMutation(); const [value, setValue] = React.useState<string>(''); const showGrid = useAppSelector(state => state.localEdits.calibrationSettings.showGrid); - const mapData = useAppSelector(state => mapsAdapter.getSelectors().selectById(state.localEdits.mapEdits, state.localEdits.calibratingMap)); + const mapData = useAppSelector(state => selectMapById(state, state.localEdits.calibratingMap)); const resultDisplay = (mapData.calibrationResult) ? `x: ${mapData.calibrationResult.maxError.x}%, y: ${mapData.calibrationResult.maxError.y}%` : translate('need.more.points'); @@ -49,7 +49,6 @@ export default function MapCalibrationInfoDisplayComponent() { longitude: array[longitudeIndex], latitude: array[latitudeIndex] }; - console.log('Verify: this.props.updateGPSCoordinates(gps); ', gps); dispatch(localEditsSlice.actions.offerCurrentGPS(gps)); resetInputField(); @@ -61,12 +60,10 @@ export default function MapCalibrationInfoDisplayComponent() { const handleGPSInput = (event: ChangeEvent<HTMLTextAreaElement>) => setValue(event.target.value); const dropCurrentCalibration = () => { - console.log('Verfiy this.props.dropCurrentCalibration();'); dispatch(localEditsSlice.actions.resetCalibration(mapData.id)); }; const handleChanges = () => { - console.log('Verfiy: // this.props.submitCalibratingMap();'); if (mapData.id < 0) { createNewMap(mapData); } else { diff --git a/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx b/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx index dd30dd9f0..8c99655cf 100644 --- a/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { ChangeEvent } from 'react'; import { FormattedMessage } from 'react-intl'; -import { updateMapMode, updateMapSource } from '../../redux/actions/map'; +import { localEditsSlice } from '../../redux/slices/localEditsSlice'; import { logsApi } from '../../redux/api/logApi'; import { selectMapById } from '../../redux/api/mapsApi'; import { useTranslate } from '../../redux/componentHooks'; @@ -92,8 +92,8 @@ export default function MapCalibrationInitiateComponent() { event.preventDefault(); try { const mapMetaData = await processImgMapMetaData(); - dispatch(updateMapSource(mapMetaData)); - dispatch(updateMapMode(CalibrationModeTypes.calibrate)); + dispatch(localEditsSlice.actions.setOneEdit(mapMetaData)); + dispatch(localEditsSlice.actions.updateMapCalibrationMode({ id: mapData.id, mode: CalibrationModeTypes.calibrate })); } catch (err) { logToServer({ level: 'error', message: `Error, map source image uploading: ${err}` }); } @@ -129,7 +129,7 @@ export default function MapCalibrationInitiateComponent() { // Fire when image load complete. img.onload = () => { // resolve mapMetadata from image. - // Not storing image in state, instead extract relevang values + // Not storing image in state, instead extract relevant values resolve({ ...mapData, imgWidth: img.width, diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index e35c321cf..f8e9d6650 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -5,12 +5,12 @@ import { parseZone } from 'moment'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { LocaleDataKey } from 'translations/data'; +import { selectMapById } from '../../redux/api/mapsApi'; import { useAppSelector } from '../../redux/reduxHooks'; +import { localEditsSlice } from '../../redux/slices/localEditsSlice'; import '../../styles/card-page.css'; import translate from '../../utils/translate'; import EditMapModalComponent from './EditMapModalComponent'; -import { selectMapById } from '../../redux/api/mapsApi'; interface MapViewProps { mapID: number; } @@ -18,11 +18,15 @@ interface MapViewProps { //TODO: Migrate to RTK const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { - const map = useAppSelector(state => selectMapById(state, mapID)); + const apiMap = useAppSelector(state => selectMapById(state, mapID)); + const localEditMap = useAppSelector(state => localEditsSlice.selectors.selectLocalEdit(state, mapID)); + + // Use local data first, if any + const mapToDisplay = localEditMap ?? apiMap; // Helper function checks map to see if it's calibrated const getCalibrationStatus = () => { - const isCalibrated = map.origin && map.opposite; + const isCalibrated = mapToDisplay.origin && mapToDisplay.opposite; return { color: isCalibrated ? 'black' : 'gray', messageId: isCalibrated ? 'map.is.calibrated' : 'map.is.not.calibrated' @@ -33,24 +37,24 @@ const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { return ( <div className="card"> <div className="identifier-container"> - {map.name} + {`${mapToDisplay.name}:${localEditMap ? ' (Unsaved Edits)' : ''}`} </div> - <div className={map.displayable.toString()}> - <b><FormattedMessage id="map.displayable" /></b> {translate(`TrueFalseType.${map.displayable.toString()}` as LocaleDataKey)} + <div className={mapToDisplay.displayable.toString()}> + <b><FormattedMessage id="map.displayable" /></b> {translate(`TrueFalseType.${mapToDisplay.displayable.toString()}`)} </div> <div className="item-container"> - <b><FormattedMessage id="map.circle.size" /></b> {map.circleSize} + <b><FormattedMessage id="map.circle.size" /></b> {mapToDisplay.circleSize} </div> <div className="item-container"> - <b><FormattedMessage id="note" /></b> {map.note ? map.note.slice(0, 29) : ''} + <b><FormattedMessage id="note" /></b> {mapToDisplay.note ? mapToDisplay.note.slice(0, 29) + ' ...' : ''} </div> <div className="item-container"> - <b><FormattedMessage id="map.filename" /></b> {map.filename} + <b><FormattedMessage id="map.filename" /></b> {mapToDisplay.filename} </div> <div className="item-container"> <b><FormattedMessage id="map.modified.date" /></b> {/* TODO I don't think this will properly internationalize. */} - {parseZone(map.modifiedDate, undefined, true).format('dddd, MMM DD, YYYY hh:mm a')} + {parseZone(apiMap.modifiedDate, undefined, true).format('dddd, MMM DD, YYYY hh:mm a')} </div> <div className="item-container"> <b><FormattedMessage id="map.calibration" /></b> @@ -59,7 +63,7 @@ const MapViewComponent: React.FC<MapViewProps> = ({ mapID }) => { </span> </div> <EditMapModalComponent - map={map} + map={mapToDisplay} /> </div> ); diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index c5a34ef17..95b5e1c7c 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -8,11 +8,11 @@ import { Link } from 'react-router-dom'; import { Button } from 'reactstrap'; import { selectMapIds } from '../../redux/api/mapsApi'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import { setNewMap } from '../../redux/actions/map'; import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; import '../../styles/card-page.css'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import MapViewComponent from './MapViewComponent'; +import { localEditsSlice } from '../../redux/slices/localEditsSlice'; /** * Defines the maps page card view @@ -22,8 +22,7 @@ import MapViewComponent from './MapViewComponent'; export default function MapsDetailComponent() { const dispatch = useAppDispatch(); // Load map IDs from state and store in number array - const maps = useAppSelector(state => selectMapIds(state)); - + const mapIds = useAppSelector(state => selectMapIds(state)); return ( <div className='flexGrowOne'> <TooltipHelpComponent page='maps' /> @@ -36,14 +35,14 @@ export default function MapsDetailComponent() { </h2> { /* TODO: Change Link to <CreateMapModalComponent /> when it is completed */} <div className="edit-btn"> - <Link to='/calibration' onClick={() => dispatch(setNewMap())}> + <Link to='/calibration' onClick={() => dispatch(localEditsSlice.actions.createNewMap())}> <Button color='primary'> <FormattedMessage id='create.map' /> </Button> </Link> </div> <div className="card-container"> - {maps.map(mapID => ( + {mapIds.map(mapID => ( <MapViewComponent key={mapID} mapID={mapID} /> ))} </div> diff --git a/src/client/app/containers/MapChartContainer.ts b/src/client/app/containers/MapChartContainer.ts deleted file mode 100644 index 28320c022..000000000 --- a/src/client/app/containers/MapChartContainer.ts +++ /dev/null @@ -1,353 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - - - -import { orderBy } from 'lodash'; -import * as moment from 'moment'; -import Plot, { PlotParams } from 'react-plotly.js'; -import { connect } from 'react-redux'; -import { DataType } from '../types/Datasources'; -import { State } from '../types/redux/state'; -import { UnitRepresentType } from '../types/redux/units'; -import { - CartesianPoint, - Dimensions, - calculateScaleFromEndpoints, - gpsToUserGrid, - itemDisplayableOnMap, - itemMapInfoOk, - normalizeImageDimensions -} from '../utils/calibration'; -import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; -import getGraphColor from '../utils/getGraphColor'; -import translate from '../utils/translate'; - -function mapStateToProps(state: State) { - const unitID = state.graph.selectedUnit; - // Map to use. - let map; - // Holds Plotly mapping info. - const data = []; - // Holds the image to use. - if (state.maps.selectedMap !== 0) { - const mapID = state.maps.selectedMap; - if (state.maps.byMapID[mapID]) { - map = state.maps.byMapID[mapID]; - if (state.maps.editedMaps[mapID]) { - map = state.maps.editedMaps[mapID]; - } - } - // Holds the hover text for each point for Plotly - const hoverText: string[] = []; - // Holds the size of each circle for Plotly. - const size: number[] = []; - // Holds the color of each circle for Plotly. - const colors: string[] = []; - // If there is no map then use a new, empty image as the map. I believe this avoids errors - // and gives the blank screen. - // Arrays to hold the Plotly grid location (x, y) for circles to place on map. - const x: number[] = []; - const y: number[] = []; - - // Figure out what time interval the bar is using since user bar data for now. - const timeInterval = state.graph.queryTimeInterval; - const barDuration = state.graph.barDuration; - // Make sure there is a map with values so avoid issues. - if (map && map.origin && map.opposite) { - // The size of the original map loaded into OED. - const imageDimensions: Dimensions = { - width: map.imgWidth, - height: map.imgHeight - }; - // Determine the dimensions so within the Plotly coordinates on the user map. - const imageDimensionNormalized = normalizeImageDimensions(imageDimensions); - // This is the origin & opposite from the calibration. It is the lower, left - // and upper, right corners of the user map. - const origin = map.origin; - const opposite = map.opposite; - // Get the GPS degrees per unit of Plotly grid for x and y. By knowing the two corners - // (or really any two distinct points) you can calculate this by the change in GPS over the - // change in x or y which is the map's width & height in this case. - const scaleOfMap = calculateScaleFromEndpoints(origin, opposite, imageDimensionNormalized, map.northAngle); - // Loop over all selected meters. Maps only work for meters at this time. - // The y-axis label depends on the unit which is in selectUnit state. - const graphingUnit = state.graph.selectedUnit; - let unitLabel: string = ''; - // If graphingUnit is -99 then none selected and nothing to graph so label is empty. - // This will probably happen when the page is first loaded. - if (graphingUnit !== -99) { - const selectUnitState = state.units.units[state.graph.selectedUnit]; - if (selectUnitState !== undefined) { - // Quantity and flow units have different unit labels. - // Look up the type of unit if it is for quantity/flow (should not be raw) and decide what to do. - // Bar graphics are always quantities. - if (selectUnitState.unitRepresent === UnitRepresentType.quantity) { - // If it is a quantity unit then that is the unit you are graphing but it is normalized to per day. - unitLabel = selectUnitState.identifier + ' / day'; - } else if (selectUnitState.unitRepresent === UnitRepresentType.flow) { - // If it is a flow meter then you need to multiply by time to get the quantity unit then show as per day. - // The quantity/time for flow has varying time so label by multiplying by time. - // To make sure it is clear, also indicate it is a quantity. - // Note this should not be used for raw data. - // It might not be usual to take a flow and make it into a quantity so this label is a little different to - // catch people's attention. If sites/users don't like OED doing this then we can eliminate flow for these types - // of graphics as we are doing for rate. - unitLabel = selectUnitState.identifier + ' * time / day ≡ quantity / day'; - } - if (state.graph.areaNormalization) { - unitLabel += ' / ' + translate(`AreaUnitType.${state.graph.selectedAreaUnit}`); - } - } - } - - for (const meterID of state.graph.selectedMeters) { - // Get meter id number. - const byMeterID = state.readings.bar.byMeterID[meterID]; - // Get meter GPS value. - const gps = state.meters.byMeterID[meterID].gps; - // filter meters with actual gps coordinates. - if (gps !== undefined && gps !== null && byMeterID !== undefined) { - let meterArea = state.meters.byMeterID[meterID].area; - // we either don't care about area, or we do in which case there needs to be a nonzero area - if (!state.graph.areaNormalization || (meterArea > 0 && state.meters.byMeterID[meterID].areaUnit != AreaUnitType.none)) { - if (state.graph.areaNormalization) { - // convert the meter area into the proper unit, if needed - meterArea *= getAreaUnitConversion(state.meters.byMeterID[meterID].areaUnit, state.graph.selectedAreaUnit); - } - // Convert the gps value to the equivalent Plotly grid coordinates on user map. - // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. - // It must be on true north map since only there are the GPS axis parallel to the map axis. - // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating - // it coordinates on the true north map and then rotating/shifting to the user map. - const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); - // Only display items within valid info and within map. - if (itemMapInfoOk(meterID, DataType.Meter, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid)) { - // The x, y value for Plotly to use that are on the user map. - x.push(meterGPSInUserGrid.x); - y.push(meterGPSInUserGrid.y); - // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed - // and be fetching. The unit could change from that menu so also need to check. - if (byMeterID[timeInterval.toString()] !== undefined && byMeterID[timeInterval.toString()][barDuration.toISOString()] !== undefined) { - // Get the bar data to use for the map circle. - const readingsData = byMeterID[timeInterval.toString()][barDuration.toISOString()][unitID]; - // This protects against there being no readings or that the data is being updated. - if (readingsData !== undefined && !readingsData.isFetching) { - // Meter name to include in hover on graph. - const label = state.meters.byMeterID[meterID].identifier; - // The usual color for this meter. - colors.push(getGraphColor(meterID, DataType.Meter)); - if (readingsData.readings === undefined) { - throw new Error('Unacceptable condition: readingsData.readings is undefined.'); - } - // Use the most recent time reading for the circle on the map. - // This has the limitations of the bar value where the last one can include ranges without - // data (GitHub issue on this). - // TODO: It might be better to do this similarly to compare. (See GitHub issue) - const readings = orderBy(readingsData.readings, ['startTimestamp'], ['desc']); - const mapReading = readings[0]; - let timeReading: string; - let averagedReading = 0; - if (readings.length === 0) { - // No data. The next lines causes an issue so set specially. - // There may be a better overall fix for no data. - timeReading = 'no data to display'; - size.push(0); - } else { - // only display a range of dates for the hover text if there is more than one day in the range - // Shift to UTC since want database time not local/browser time which is what moment does. - timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`; - if (barDuration.asDays() != 1) { - // subtracting one extra day caused by day ending at midnight of the next day. - // Going from DB unit timestamp that is UTC so force UTC with moment, as usual. - timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`; - } - // The value for the circle is the average daily usage. - averagedReading = mapReading.reading / barDuration.asDays(); - if (state.graph.areaNormalization) { - averagedReading /= meterArea; - } - // The size is the reading value. It will be scaled later. - size.push(averagedReading); - } - // The hover text. - hoverText.push(`<b> ${timeReading} </b> <br> ${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`); - } - } - } - } - } - } - - for (const groupID of state.graph.selectedGroups) { - // Get group id number. - const byGroupID = state.readings.bar.byGroupID[groupID]; - // Get group GPS value. - const gps = state.groups.byGroupID[groupID].gps; - // Filter groups with actual gps coordinates. - if (gps !== undefined && gps !== null && byGroupID !== undefined) { - let groupArea = state.groups.byGroupID[groupID].area; - if (!state.graph.areaNormalization || (groupArea > 0 && state.groups.byGroupID[groupID].areaUnit != AreaUnitType.none)) { - if (state.graph.areaNormalization) { - // convert the meter area into the proper unit, if needed - groupArea *= getAreaUnitConversion(state.groups.byGroupID[groupID].areaUnit, state.graph.selectedAreaUnit); - } - // Convert the gps value to the equivalent Plotly grid coordinates on user map. - // First, convert from GPS to grid units. Since we are doing a GPS calculation, this happens on the true north map. - // It must be on true north map since only there are the GPS axis parallel to the map axis. - // To start, calculate the user grid coordinates (Plotly) from the GPS value. This involves calculating - // it coordinates on the true north map and then rotating/shifting to the user map. - const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, map.northAngle); - // Only display items within valid info and within map. - if (itemMapInfoOk(groupID, DataType.Group, map, gps) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid)) { - // The x, y value for Plotly to use that are on the user map. - x.push(groupGPSInUserGrid.x); - y.push(groupGPSInUserGrid.y); - // Make sure the bar reading data is available. The timeInterval should be fine (but checked) but the barDuration might have changed - // and be fetching. The unit could change from that menu so also need to check. - if (byGroupID[timeInterval.toString()] !== undefined && byGroupID[timeInterval.toString()][barDuration.toISOString()] !== undefined) { - // Get the bar data to use for the map circle. - const readingsData = byGroupID[timeInterval.toString()][barDuration.toISOString()][unitID]; - // This protects against there being no readings or that the data is being updated. - if (readingsData !== undefined && !readingsData.isFetching) { - // Group name to include in hover on graph. - const label = state.groups.byGroupID[groupID].name; - // The usual color for this group. - colors.push(getGraphColor(groupID, DataType.Group)); - if (readingsData.readings === undefined) { - throw new Error('Unacceptable condition: readingsData.readings is undefined.'); - } - // Use the most recent time reading for the circle on the map. - // This has the limitations of the bar value where the last one can include ranges without - // data (GitHub issue on this). - // TODO: It might be better to do this similarly to compare. (See GitHub issue) - const readings = orderBy(readingsData.readings, ['startTimestamp'], ['desc']); - const mapReading = readings[0]; - let timeReading: string; - let averagedReading = 0; - if (readings.length === 0) { - // No data. The next lines causes an issue so set specially. - // There may be a better overall fix for no data. - timeReading = 'no data to display'; - size.push(0); - } else { - // only display a range of dates for the hover text if there is more than one day in the range - timeReading = `${moment.utc(mapReading.startTimestamp).format('ll')}`; - if (barDuration.asDays() != 1) { - // subtracting one extra day caused by day ending at midnight of the next day. - // Going from DB unit timestamp that is UTC so force UTC with moment, as usual. - timeReading += ` - ${moment.utc(mapReading.endTimestamp).subtract(1, 'days').format('ll')}`; - } - // The value for the circle is the average daily usage. - averagedReading = mapReading.reading / barDuration.asDays(); - if (state.graph.areaNormalization) { - averagedReading /= groupArea; - } - // The size is the reading value. It will be scaled later. - size.push(averagedReading); - } - // The hover text. - hoverText.push(`<b> ${timeReading} </b> <br> ${label}: ${averagedReading.toPrecision(6)} ${unitLabel}`); - } - } - } - } - } - } - // TODO Using the following seems to have no impact on the code. It has been noticed that this function is called - // many times for each change. Someone should look at why that is happening and why some have no items in the arrays. - // if (size.length > 0) { - // TODO The max circle diameter should come from admin/DB. - const maxFeatureFraction = map.circleSize; - // Find the smaller of width and height. This is used since it means the circle size will be - // scaled to that dimension and smaller relative to the other coordinate. - const minDimension = Math.min(imageDimensionNormalized.width, imageDimensionNormalized.height); - // The circle size is set to area below. Thus, we need to convert from wanting a max - // diameter of minDimension * maxFeatureFraction to an area. - const maxCircleSize = Math.PI * Math.pow(minDimension * maxFeatureFraction / 2, 2); - // Find the largest circle which is usage. - const largestCircleSize = Math.max(...size); - // Scale largest circle to the max size and others will be scaled to be smaller. - // Not that < 1 => a larger circle. - const scaling = largestCircleSize / maxCircleSize; - - // Per https://plotly.com/javascript/reference/scatter/: - // The opacity of 0.5 makes it possible to see the map even when there is a circle but the hover - // opacity is 1 so it is easy to see. - // Set the sizemode to area not diameter. - // Set the sizemin so a circle cannot get so small that it might disappear. Unsure the best size. - // Set the sizeref to scale each point to the desired area. - // Note all sizes are in px so have to estimate the actual size. This could be an issue but maps are currently - // a fixed size so not too much of an issue. - // Also note that the circle can go off the edge of the map. At some point it would be nice to have a border - // around the map to avoid this. - const traceOne = { - x, - y, - type: 'scatter', - mode: 'markers', - marker: { - color: colors, - opacity: 0.5, - size, - sizemin: 6, - sizeref: scaling, - sizemode: 'area' - }, - text: hoverText, - hoverinfo: 'text', - opacity: 1, - showlegend: false - }; - data.push(traceOne); - } - } - - // set map background image - const layout: any = { - // Either the actual map name or text to say it is not available. - title: { - text: (map) ? map.name : translate('map.unavailable') - }, - width: 1000, - height: 1000, - xaxis: { - visible: false, // changes all visibility settings including showgrid, zeroline, showticklabels and hiding ticks - range: [0, 500] // range of displayed graph - }, - yaxis: { - visible: false, - range: [0, 500], - scaleanchor: 'x' - }, - images: [{ - layer: 'below', - source: map?.mapSource, - xref: 'x', - yref: 'y', - x: 0, - y: 0, - sizex: 500, - sizey: 500, - xanchor: 'left', - yanchor: 'bottom', - sizing: 'contain', - opacity: 1 - }] - }; - - /*** - * Usage: - * <Plot data={toJS(this.model_data)} - * layout={layout} - * onClick={({points, event}) => console.log(points, event)}> - */ - const props = { - data, - layout - } as PlotParams; - return props; -} - -export default connect(mapStateToProps)(Plot); diff --git a/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts b/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts deleted file mode 100644 index 64208c7fe..000000000 --- a/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts +++ /dev/null @@ -1,173 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { connect } from 'react-redux'; -import Plot from 'react-plotly.js'; -import { State } from '../../types/redux/state'; -import * as plotly from 'plotly.js'; -import { CartesianPoint, Dimensions, normalizeImageDimensions } from '../../utils/calibration'; -import { updateCurrentCartesian } from '../../redux/actions/map'; -import { store } from '../../store'; -import { CalibrationSettings } from '../../types/redux/map'; -import Locales from '../../types/locales'; - -function mapStateToProps(state: State) { - const x: number[] = []; - const y: number[] = []; - const texts: string[] = []; - - const mapID = state.maps.calibratingMap; - const map = state.maps.editedMaps[mapID]; - const points = map.calibrationSet; - if (points) { - for (const point of points) { - x.push(point.cartesian.x); - y.push(point.cartesian.y); - texts.push(`latitude: ${point.gps.latitude}, longitude: ${point.gps.longitude}`); - } - } - const imageDimensions: Dimensions = normalizeImageDimensions({ - width: map.imgWidth, - height: map.imgHeight - }); - const settings = state.maps.calibrationSettings; - const backgroundTrace = createBackgroundTrace(imageDimensions, settings); - const dataPointTrace = { - x, - y, - type: 'scatter', - mode: 'markers', - marker: { - color: 'rgb(7,110,180)', - opacity: 0.5, - size: 6 - }, - text: texts, - opacity: 1, - showlegend: false - }; - const data = [backgroundTrace, dataPointTrace]; - - const imageSource = map.mapSource; - - // for a detailed description of layout attributes: https://plotly.com/javascript/reference/#layout - const layout: any = { - width: 1000, - height: 1000, - xaxis: { - visible: false, // changes all visibility settings including showgrid, zeroline, showticklabels and hiding ticks - range: [0, 500] // range of displayed graph - }, - yaxis: { - visible: false, - range: [0, 500], - scaleanchor: 'x' - }, - images: [{ - layer: 'below', - source: imageSource, - xref: 'x', - yref: 'y', - x: 0, - y: 0, - sizex: 500, - sizey: 500, - xanchor: 'left', - yanchor: 'bottom', - sizing: 'contain', - opacity: 1 - }] - }; - - /*** - * Usage: - * <Plot data={toJS(this.model_data)} - * layout={layout} - * onClick={({points, event}) => console.log(points, event)}> - * Plotly no longer has IPlotlyChartProps so we will use any for now. - */ - const props: any = { - data, - layout, - onClick: (event: plotly.PlotMouseEvent) => handlePointClick(event), - config: { - locales: Locales // makes locales available for use - } - }; - props.config.locale = state.appState.selectedLanguage; - return props; -} - -/** - * use a transparent heatmap to capture which point the user clicked on the map - * @param imageDimensions Normalized dimensions of the image - * @param settings Settings for calibration displays - * @returns point and data - */ -function createBackgroundTrace(imageDimensions: Dimensions, settings: CalibrationSettings) { - // define the grid of heatmap - const x: number[] = []; - const y: number[] = []; - // bound the grid to image dimensions to avoid clicking outside of the map - for (let i = 0; i <= Math.ceil(imageDimensions.width); i = i + 1) { - x.push(i); - } - for (let j = 0; j <= Math.ceil(imageDimensions.height); j = j + 1) { - y.push(j); - } - // define the actual points of the graph, numbers in the array are used to designate different colors; - const z: number[][] = []; - for (let ind1 = 0; ind1 < y.length; ++ind1) { - const temp = []; - for (let ind2 = 0; ind2 < x.length; ++ind2) { - temp.push(0); - } - z.push(temp); - } - const trace = { - x, - y, - z, - type: 'heatmap', - colorscale: [['0.5', 'rgba(6,86,157,0)']], // set colors to be fully transparent - xgap: 1, - ygap: 1, - hoverinfo: 'x+y', - opacity: (settings.showGrid) ? '0.5' : '0', // controls whether the grids will be displayed - showscale: false - }; - return trace; -} - -function handlePointClick(event: plotly.PlotMouseEvent) { - event.event.preventDefault(); - const currentPoint: CartesianPoint = getClickedCoordinates(event); - store.dispatch(updateCurrentCartesian(currentPoint)); -} - -function getClickedCoordinates(event: plotly.PlotMouseEvent) { - event.event.preventDefault(); - /* - * trace 0 keeps a transparent trace of closely positioned points used for calibration(backgroundTrace), - * trace 1 keeps the data points used for calibration are automatically added to the same trace(dataPointTrace), - * event.points will include all points near a mouse click, including those in the backgroundTrace and the dataPointTrace, - * so the algorithm only looks at trace 0 since points from trace 1 are already put into the data set used for calibration. - */ - const eligiblePoints = []; - for (const point of event.points) { - const traceNumber = point.curveNumber; - if (traceNumber === 0) { - eligiblePoints.push(point); - } - } - const xValue = eligiblePoints[0].x as number; - const yValue = eligiblePoints[0].y as number; - const clickedPoint: CartesianPoint = { - x: Number(xValue.toFixed(6)), - y: Number(yValue.toFixed(6)) - }; - return clickedPoint; -} - -export default connect(mapStateToProps)(Plot); diff --git a/src/client/app/containers/maps/MapCalibrationContainer.ts b/src/client/app/containers/maps/MapCalibrationContainer.ts deleted file mode 100644 index fd927bd35..000000000 --- a/src/client/app/containers/maps/MapCalibrationContainer.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import {connect} from 'react-redux'; -import {State} from '../../types/redux/state'; -import MapCalibrationComponent from '../../components/maps/MapCalibrationComponent'; -import {CalibrationModeTypes} from '../../types/redux/map'; - -function mapStateToProps(state: State) { - const mapID = state.maps.calibratingMap; - return { - mode: (state.maps.editedMaps[mapID]) ? state.maps.editedMaps[mapID].calibrationMode : CalibrationModeTypes.unavailable, - isLoading: false, - mapID - }; -} - -export default connect(mapStateToProps)(MapCalibrationComponent); diff --git a/src/client/app/containers/maps/MapCalibrationInfoDisplayContainer.ts b/src/client/app/containers/maps/MapCalibrationInfoDisplayContainer.ts deleted file mode 100644 index 94e27983c..000000000 --- a/src/client/app/containers/maps/MapCalibrationInfoDisplayContainer.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import {connect} from 'react-redux'; -import { State } from '../../types/redux/state'; -import { Dispatch } from '../../types/redux/actions'; -import MapCalibrationInfoDisplayComponent from '../../components/maps/MapCalibrationInfoDisplayComponent'; -import {changeGridDisplay, dropCalibration, offerCurrentGPS, submitCalibratingMap} from '../../redux/actions/map'; -import {GPSPoint} from '../../utils/calibration'; -import {logToServer} from '../../redux/actions/logs'; -import translate from '../../utils/translate'; - -function mapStateToProps(state: State) { - const mapID = state.maps.calibratingMap; - const map = state.maps.editedMaps[mapID]; - const resultDisplay = (map.calibrationResult) ? - `x: ${map.calibrationResult.maxError.x}%, y: ${map.calibrationResult.maxError.y}%` - : translate('need.more.points'); - const currentCartesianDisplay = (map.currentPoint) ? - `x: ${map.currentPoint.cartesian.x}, y: ${map.currentPoint.cartesian.y}` : translate('undefined'); - return { - showGrid: state.maps.calibrationSettings.showGrid, - currentCartesianDisplay, - resultDisplay - }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - updateGPSCoordinates: (gpsCoordinate: GPSPoint) => dispatch(offerCurrentGPS(gpsCoordinate)), - submitCalibratingMap: () => dispatch(submitCalibratingMap()), - dropCurrentCalibration: () => dispatch(dropCalibration()), - log: (level: string, message: string) => dispatch(logToServer(level, message)), - changeGridDisplay: () => dispatch(changeGridDisplay()) - }; -} -export default connect(mapStateToProps, mapDispatchToProps)(MapCalibrationInfoDisplayComponent); diff --git a/src/client/app/containers/maps/MapCalibrationInitiateContainer.ts b/src/client/app/containers/maps/MapCalibrationInitiateContainer.ts deleted file mode 100644 index 647f2a6df..000000000 --- a/src/client/app/containers/maps/MapCalibrationInitiateContainer.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import MapCalibrationInitiateComponent from '../../components/maps/MapCalibrationInitiateComponent'; -import { connect } from 'react-redux'; -import { Dispatch } from '../../types/redux/actions'; -import {updateMapMode, updateMapSource} from '../../redux/actions/map'; -import {CalibrationModeTypes, MapMetadata} from '../../types/redux/map'; -import {State} from '../../types/redux/state'; - -function mapStateToProps(state: State) { - return { - map: state.maps.editedMaps[state.maps.calibratingMap] - }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - updateMapMode: (nextMode: CalibrationModeTypes) => dispatch(updateMapMode(nextMode)), - onSourceChange: (data: MapMetadata) => dispatch(updateMapSource(data)) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(MapCalibrationInitiateComponent); diff --git a/src/client/app/containers/maps/MapViewContainer.tsx b/src/client/app/containers/maps/MapViewContainer.tsx deleted file mode 100644 index 7fdecbac8..000000000 --- a/src/client/app/containers/maps/MapViewContainer.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { connect } from 'react-redux'; -import MapViewComponent from '../../components/maps/MapViewComponent'; -import { Dispatch } from '../../types/redux/actions'; -import { State } from '../../types/redux/state'; -import {CalibrationModeTypes, MapMetadata} from '../../types/redux/map'; -import {editMapDetails, removeMap, setCalibration} from '../../redux/actions/map'; - -function mapStateToProps(state: State, ownProps: {id: number}) { - let map = state.maps.byMapID[ownProps.id]; - if (state.maps.editedMaps[ownProps.id]) { - map = state.maps.editedMaps[ownProps.id]; - } - return { - map, - isEdited: state.maps.editedMaps[ownProps.id] !== undefined, - isSubmitting: state.maps.submitting.indexOf(ownProps.id) !== -1 - }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - editMapDetails: (map: MapMetadata) => dispatch(editMapDetails(map)), - setCalibration: (mode: CalibrationModeTypes, mapID: number) => dispatch(setCalibration(mode, mapID)), - removeMap: (id: number) => dispatch(removeMap(id)) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(MapViewComponent); diff --git a/src/client/app/containers/maps/MapsDetailContainer.tsx b/src/client/app/containers/maps/MapsDetailContainer.tsx deleted file mode 100644 index 011a82e59..000000000 --- a/src/client/app/containers/maps/MapsDetailContainer.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { connect } from 'react-redux'; -import { State } from '../../types/redux/state'; -import {Dispatch} from '../../types/redux/actions'; -import {fetchMapsDetails, setNewMap, submitEditedMaps} from '../../redux/actions/map'; -import MapsDetailComponent from '../../components/maps/MapsDetailComponent'; - -function mapStateToProps(state: State) { - return { - maps: Object.keys(state.maps.byMapID) - .map(key => parseInt(key)) - .filter(key => !isNaN(key)), - unsavedChanges: Object.keys(state.maps.editedMaps).length > 0 - }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - fetchMapsDetails: () => dispatch(fetchMapsDetails()), - submitEditedMaps: () => dispatch(submitEditedMaps()), - createNewMap: () => dispatch(setNewMap()) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(MapsDetailComponent); diff --git a/src/client/app/redux/actions/map.ts b/src/client/app/redux/actions/map.ts index 06882af69..ae5e0a5d3 100644 --- a/src/client/app/redux/actions/map.ts +++ b/src/client/app/redux/actions/map.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-ignore +// @ts-nocheck /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ @@ -8,7 +11,6 @@ import * as moment from 'moment'; import { ActionType, Dispatch, GetState, Thunk } from '../../types/redux/actions'; import * as t from '../../types/redux/map'; import { CalibrationModeTypes, MapData, MapMetadata } from '../../types/redux/map'; -import { State } from '../../types/redux/state'; import ApiBackend from '../../utils/api/ApiBackend'; import MapsApi from '../../utils/api/MapsApi'; import { calibrate, CalibratedPoint, CalibrationResult, CartesianPoint, GPSPoint } from '../../utils/calibration'; @@ -16,6 +18,7 @@ import { browserHistory } from '../../utils/history'; import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; import { logToServer } from './logs'; +import { RootState } from 'store'; const mapsApi = new MapsApi(new ApiBackend()); @@ -135,8 +138,8 @@ export function updateCurrentCartesian(currentCartesian: CartesianPoint): t.Upda */ export function offerCurrentGPS(currentGPS: GPSPoint): Thunk { return (dispatch: Dispatch, getState: GetState) => { - const mapID = getState().maps.calibratingMap; - const point = getState().maps.editedMaps[mapID].currentPoint; + const mapID = getState().localEdits.calibratingMap; + const point = getState().localEdits.mapEdits.entities[mapID].currentPoint; if (point && hasCartesian(point)) { point.gps = currentGPS; dispatch(updateCalibrationSet(point)); @@ -173,12 +176,10 @@ function updateCalibrationSet(calibratedPoint: CalibratedPoint): t.AppendCalibra * @param state The redux state * @returns Result of safety check */ -function isReadyForCalculation(state: State): boolean { +function isReadyForCalculation(state: RootState): boolean { const calibrationThreshold = 3; // assume calibrationSet is defined, as offerCurrentGPS indicates through point that the map is defined. - /* eslint-disable @typescript-eslint/no-non-null-assertion */ - return state.maps.editedMaps[state.maps.calibratingMap].calibrationSet!.length >= calibrationThreshold; - /* eslint-enable @typescript-eslint/no-non-null-assertion */ + return state.localEdits.mapEdits.entities[state.localEdits.calibratingMap].calibrationSet.length >= calibrationThreshold; } /** @@ -186,9 +187,10 @@ function isReadyForCalculation(state: State): boolean { * @param state The redux state * @returns Result of map calibration */ -function prepareDataToCalculation(state: State): CalibrationResult { - const mapID = state.maps.calibratingMap; - const mp = state.maps.editedMaps[mapID]; +function prepareDataToCalculation(state: RootState): CalibrationResult { + // TODO FIX BEFORE PR + const mapID = state.localEdits.calibratingMap; + const mp = state.localEdits.mapEdits.entities[mapID]; // Since mp is defined above, calibrationSet is defined. /* eslint-disable @typescript-eslint/no-non-null-assertion */ const result = calibrate(mp); diff --git a/src/client/app/redux/api/groupsApi.ts b/src/client/app/redux/api/groupsApi.ts index cc73eae5f..37bdebcad 100644 --- a/src/client/app/redux/api/groupsApi.ts +++ b/src/client/app/redux/api/groupsApi.ts @@ -2,19 +2,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { EntityState, Update, createEntityAdapter } from '@reduxjs/toolkit'; +import { Update } from '@reduxjs/toolkit'; import { omit } from 'lodash'; +import { GroupDataState, groupsAdapter, groupsInitialState } from '../../redux/entityAdapters'; import { RootState } from '../../store'; import { GroupChildren, GroupData } from '../../types/redux/groups'; import { showErrorNotification } from '../../utils/notifications'; import { selectIsAdmin } from '../slices/currentUserSlice'; import { baseApi } from './baseApi'; -export const groupsAdapter = createEntityAdapter<GroupData>({ - sortComparer: (groupA, groupB) => groupA.name?.localeCompare(groupB.name, undefined, { sensitivity: 'accent' }) -}); -export const groupsInitialState = groupsAdapter.getInitialState(); -export type GroupDataState = EntityState<GroupData, number>; export const groupsApi = baseApi.injectEndpoints({ endpoints: builder => ({ diff --git a/src/client/app/redux/api/mapsApi.ts b/src/client/app/redux/api/mapsApi.ts index 7a8f7addd..93a82d1eb 100644 --- a/src/client/app/redux/api/mapsApi.ts +++ b/src/client/app/redux/api/mapsApi.ts @@ -1,13 +1,13 @@ -import { createEntityAdapter, EntityState } from '@reduxjs/toolkit'; import { pick } from 'lodash'; import * as moment from 'moment'; +import { MapDataState, mapsAdapter, mapsInitialState } from '../../redux/entityAdapters'; import { createAppSelector } from '../../redux/selectors/selectors'; +import { emtpyMapMetadata, localEditsSlice } from '../../redux/slices/localEditsSlice'; import { RootState } from '../../store'; import { MapData, MapMetadata } from '../../types/redux/map'; import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; import { baseApi } from './baseApi'; -// import { logToServer } from '../../redux/actions/logs'; // Helper function to extract image dimensions from the mapSource const mapResponseImgSrcToDimensions = (response: MapMetadata[]) => Promise.all( @@ -15,11 +15,10 @@ const mapResponseImgSrcToDimensions = (response: MapMetadata[]) => Promise.all( new Promise<MapMetadata>(resolve => { const img = new Image(); img.onload = () => { - resolve({ ...mapData, imgWidth: img.width, imgHeight: img.height }); + resolve({ ...emtpyMapMetadata, ...mapData, imgWidth: img.width, imgHeight: img.height }); }; img.onerror = () => { - // TODO default to falsy value, 0, on error. - resolve({ ...mapData, imgWidth: 0, imgHeight: 0 }); + resolve({ ...emtpyMapMetadata, ...mapData }); }; img.src = mapData.mapSource; }) @@ -27,13 +26,6 @@ const mapResponseImgSrcToDimensions = (response: MapMetadata[]) => Promise.all( ); -export const mapsAdapter = createEntityAdapter<MapMetadata>({ - sortComparer: (meterA, meterB) => meterA.name?.localeCompare(meterB.name, undefined, { sensitivity: 'accent' }) - -}); -export const mapsInitialState = mapsAdapter.getInitialState(); -export type MapDataState = EntityState<MapMetadata, number>; - export const mapsApi = baseApi.injectEndpoints({ endpoints: build => ({ @@ -116,7 +108,7 @@ export const mapsApi = baseApi.injectEndpoints({ showSuccessNotification(translate('updated.map.without.calibration')); } // Cleanup LocalEditsSLice - // api.dispatch(localEditsSlice.actions.removeOneEdit({ type: EntityType.MAP, id: map.id })); + api.dispatch(localEditsSlice.actions.removeOneEdit(map.id)); }).catch(() => { showErrorNotification(translate('failed.to.edit.map')); }); @@ -128,7 +120,16 @@ export const mapsApi = baseApi.injectEndpoints({ url: 'api/maps/delete', method: 'POST', body: { id } - }) + }), + onQueryStarted: (arg, api) => { + api.queryFulfilled + //Cleanup Local Edits if any for deleted entity + .then(() => { + api.dispatch(localEditsSlice.actions.removeOneEdit(arg)); + }) + .catch(); + }, + invalidatesTags: ['MapsData'] }), getMapById: build.query<MapData, number>({ query: id => `api/maps/${id}` @@ -151,5 +152,3 @@ export const selectMapSelectOptions = createAppSelector( allMaps => allMaps.map(map => ( { value: map.id, label: map.name, isDisabled: !(map.origin && map.opposite) } ))); - - diff --git a/src/client/app/redux/api/metersApi.ts b/src/client/app/redux/api/metersApi.ts index 5e1a55519..8bb7c263f 100644 --- a/src/client/app/redux/api/metersApi.ts +++ b/src/client/app/redux/api/metersApi.ts @@ -2,7 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { EntityState, createEntityAdapter } from '@reduxjs/toolkit'; import { NamedIDItem } from 'types/items'; import { RawReadings } from 'types/readings'; import { TimeInterval } from '../../../../common/TimeInterval'; @@ -11,13 +10,8 @@ import { MeterData } from '../../types/redux/meters'; import { durationFormat } from '../../utils/durationFormat'; import { baseApi } from './baseApi'; import { conversionsApi } from './conversionsApi'; +import { MeterDataState, meterAdapter, metersInitialState } from '../../redux/entityAdapters'; -export const meterAdapter = createEntityAdapter<MeterData>({ - sortComparer: (meterA, meterB) => meterA.identifier?.localeCompare(meterB.identifier, undefined, { sensitivity: 'accent' }) - -}); -export const metersInitialState = meterAdapter.getInitialState(); -export type MeterDataState = EntityState<MeterData, number>; export const metersApi = baseApi.injectEndpoints({ endpoints: builder => ({ diff --git a/src/client/app/redux/api/unitsApi.ts b/src/client/app/redux/api/unitsApi.ts index eae58d7a4..ae662c909 100644 --- a/src/client/app/redux/api/unitsApi.ts +++ b/src/client/app/redux/api/unitsApi.ts @@ -2,16 +2,11 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { EntityState, createEntityAdapter } from '@reduxjs/toolkit'; import { RootState } from 'store'; import { UnitData, UnitDataById } from '../../types/redux/units'; import { baseApi } from './baseApi'; import { conversionsApi } from './conversionsApi'; -export const unitsAdapter = createEntityAdapter<UnitData>({ - sortComparer: (unitA, unitB) => unitA.identifier?.localeCompare(unitB.identifier, undefined, { sensitivity: 'accent' }) -}); -export const unitsInitialState = unitsAdapter.getInitialState(); -export type UnitDataState = EntityState<UnitData, number>; +import { UnitDataState, unitsAdapter, unitsInitialState } from '../../redux/entityAdapters'; export const unitsApi = baseApi.injectEndpoints({ endpoints: builder => ({ diff --git a/src/client/app/redux/devToolConfig.ts b/src/client/app/redux/devToolConfig.ts index 70d5a31b0..02fe511bc 100644 --- a/src/client/app/redux/devToolConfig.ts +++ b/src/client/app/redux/devToolConfig.ts @@ -1,5 +1,6 @@ import { DevToolsEnhancerOptions } from '@reduxjs/toolkit'; -import { mapsAdapter, mapsApi, mapsInitialState } from './api/mapsApi'; +import { mapsApi } from './api/mapsApi'; +import { mapsAdapter, mapsInitialState } from './entityAdapters'; export const devToolsConfig: DevToolsEnhancerOptions = { actionSanitizer: action => { diff --git a/src/client/app/redux/entityAdapters.ts b/src/client/app/redux/entityAdapters.ts new file mode 100644 index 000000000..92402a2ee --- /dev/null +++ b/src/client/app/redux/entityAdapters.ts @@ -0,0 +1,45 @@ +import { EntityState, createEntityAdapter } from '@reduxjs/toolkit'; +import { ConversionData } from '../types/redux/conversions'; +import { GroupData } from '../types/redux/groups'; +import { MapMetadata } from '../types/redux/map'; +import { MeterData } from '../types/redux/meters'; +import { UnitData } from '../types/redux/units'; +const sortByIdentifierProperty = (a: any, b: any) => a.identifier?.localeCompare(b.identifier, undefined, { sensitivity: 'accent' }); +const sortByNameProperty = (a: any, b: any) => a.name?.localeCompare(b.name, undefined, { sensitivity: 'accent' }); + +// Adapters re-homed for compatability with localEditsSlice.ts/ prevents circular dependency issues. +// Meters +export const meterAdapter = createEntityAdapter<MeterData>({ sortComparer: sortByIdentifierProperty }); +export const metersInitialState = meterAdapter.getInitialState(); +export type MeterDataState = EntityState<MeterData, number>; + + +// Units +export const unitsAdapter = createEntityAdapter<UnitData>({ sortComparer: sortByIdentifierProperty }); +export const unitsInitialState = unitsAdapter.getInitialState(); +export type UnitDataState = EntityState<UnitData, number>; + +// Groups +export const groupsAdapter = createEntityAdapter<GroupData>({ sortComparer: sortByNameProperty }); +export const groupsInitialState = groupsAdapter.getInitialState(); +export type GroupDataState = EntityState<GroupData, number>; + + +// Maps +export const mapsAdapter = createEntityAdapter<MapMetadata>({ sortComparer: sortByNameProperty }); +export const mapsInitialState = mapsAdapter.getInitialState(); +export type MapDataState = EntityState<MapMetadata, number>; + +// Conversions +// Extending conversion data to add an id number +// Conversions are stored in the database as a composite key of source/destination. EntityAdapter requires a unique ID, +export type ConversionDataWithIds = ConversionData & { id: number }; +// This is exclusively for the front end to take advantage of the entity adapter and its derived selectors. +// So Adding the id property as the response's array index +// Will not impact backend/server +// Conversions sorts using unitData values, which is not possible with entity adapters so sort by synthetic id +// Will have to sort by 'id' (default, no sort comparer) +export const conversionsAdapter = createEntityAdapter<ConversionDataWithIds>(); +export const conversionsInitialState = conversionsAdapter.getInitialState(); +export type ConversionDataState = EntityState<ConversionDataWithIds, number>; + diff --git a/src/client/app/redux/slices/appStateSlice.ts b/src/client/app/redux/slices/appStateSlice.ts index a96db219b..547249212 100644 --- a/src/client/app/redux/slices/appStateSlice.ts +++ b/src/client/app/redux/slices/appStateSlice.ts @@ -75,16 +75,12 @@ export const appStateSlice = createThunkSlice({ if (hasToken()) { // User has a session token verify before requesting meter/group details try { - await dispatch(authApi.endpoints.verifyToken.initiate(getToken())) - .unwrap() - .catch(e => { throw e; }); + await dispatch(authApi.endpoints.verifyToken.initiate(getToken())).unwrap(); // Token is valid if not errored out by this point, // Apis will now use the token in headers via baseAPI's Prepare Headers dispatch(currentUserSlice.actions.setUserToken(getToken())); // Get userDetails with verified token in headers - await dispatch(userApi.endpoints.getUserDetails.initiate(undefined, { subscribe: false })) - .unwrap() - .catch(e => { throw e; }); + await dispatch(userApi.endpoints.getUserDetails.initiate(undefined, { subscribe: false })).unwrap(); } catch { // User had a token that isn't valid or getUserDetails threw an error. diff --git a/src/client/app/redux/slices/localEditsSlice.ts b/src/client/app/redux/slices/localEditsSlice.ts index 7845a1811..c9e9002c1 100644 --- a/src/client/app/redux/slices/localEditsSlice.ts +++ b/src/client/app/redux/slices/localEditsSlice.ts @@ -1,12 +1,13 @@ import { createEntityAdapter } from '@reduxjs/toolkit'; +import { PlotMouseEvent } from 'plotly.js'; import { hasCartesian } from '../../redux/actions/map'; import { createThunkSlice } from '../../redux/sliceCreators'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; import { calibrate, CalibratedPoint, CartesianPoint, GPSPoint } from '../../utils/calibration'; -import { mapsAdapter } from '../../redux/api/mapsApi'; -import { PlotMouseEvent } from 'plotly.js'; +import { mapsAdapter } from '../../redux/entityAdapters'; const localEditAdapter = createEntityAdapter<MapMetadata>(); +const localSelectors = localEditAdapter.getSelectors(); export const localEditsSlice = createThunkSlice({ name: 'localEdits', initialState: { @@ -17,9 +18,7 @@ export const localEditsSlice = createThunkSlice({ calibrationSettings: { calibrationThreshold: 3, showGrid: false - } - }, reducers: create => ({ incrementCounter: create.reducer<void>(state => { @@ -28,17 +27,32 @@ export const localEditsSlice = createThunkSlice({ toggleMapShowGrid: create.reducer<void>(state => { state.calibrationSettings.showGrid; }), + setOneEdit: create.reducer<MapMetadata>((state, { payload }) => { + localEditAdapter.setOne(state.mapEdits, payload); + }), + removeOneEdit: create.reducer<number>((state, { payload }) => { + localEditAdapter.removeOne(state.mapEdits, payload); + }), + updateMapCalibrationMode: create.reducer<{ id: number, mode: CalibrationModeTypes }>((state, { payload }) => { + state.calibratingMap = payload.id; + localEditAdapter.updateOne(state.mapEdits, { + id: payload.id, + changes: { calibrationMode: payload.mode } + }); + }), createNewMap: create.reducer(state => { state.newMapIdCounter++; const temporaryID = state.newMapIdCounter * -1; - localEditAdapter.setOne(state.mapEdits, { ...emptyMetadata, id: temporaryID }); + localEditAdapter.setOne(state.mapEdits, { ...emtpyMapMetadata, id: temporaryID }); state.calibratingMap = temporaryID; }), offerCurrentGPS: create.reducer<GPSPoint>((state, { payload }) => { // Stripped offerCurrentGPS thunk into a single reducer for simplicity. The only missing functionality are the serverlogs // Current axios approach doesn't require dispatch, however if moved to rtk will. thunks for this adds complexity // For simplicity, these logs can instead be tabulated in a middleware.(probably.) - const map = localEditAdapter.getSelectors().selectById(state.mapEdits, state.calibratingMap); + // const map = localEditAdapter.getSelectors().selectById(state.mapEdits, state.calibratingMap); + const map = state.mapEdits.entities[state.calibratingMap]; + const point = map.currentPoint; if (point && hasCartesian(point)) { point.gps = payload; @@ -63,8 +77,9 @@ export const localEditsSlice = createThunkSlice({ eligiblePoints.push(point); } } - const xValue = eligiblePoints[0].x as number; - const yValue = eligiblePoints[0].y as number; + // TODO VERIFY + const xValue = eligiblePoints[0]?.x as number; + const yValue = eligiblePoints[0]?.y as number; const clickedPoint: CartesianPoint = { x: Number(xValue.toFixed(6)), y: Number(yValue.toFixed(6)) @@ -80,8 +95,6 @@ export const localEditsSlice = createThunkSlice({ id: state.calibratingMap, changes: { currentPoint } }); - - }), resetCalibration: create.reducer<number>((state, { payload }) => { localEditAdapter.updateOne(state.mapEdits, { @@ -96,12 +109,11 @@ export const localEditsSlice = createThunkSlice({ }), selectors: { - selectCalibrationMapId: state => state.calibratingMap + selectCalibrationMapId: state => state.calibratingMap, + selectLocalEdit: (state, id: number) => localSelectors.selectById(state.mapEdits, id) } }); - -// MAP Stuff TODO RELOCATE -const emptyMetadata: MapMetadata = { +export const emtpyMapMetadata: MapMetadata = { id: 0, name: '', displayable: false, @@ -119,4 +131,4 @@ const emptyMetadata: MapMetadata = { calibrationResult: undefined, northAngle: 0, circleSize: 0 -}; +}; \ No newline at end of file diff --git a/src/client/app/types/redux/actions.ts b/src/client/app/types/redux/actions.ts index 5ab356b7d..d72af3afc 100644 --- a/src/client/app/types/redux/actions.ts +++ b/src/client/app/types/redux/actions.ts @@ -4,7 +4,7 @@ import { Action } from 'redux'; import { ThunkAction, ThunkDispatch } from 'redux-thunk'; -import { State } from './state'; +import { RootState } from 'store'; export enum ActionType { @@ -35,15 +35,15 @@ export enum ActionType { * The type of the redux-thunk dispatch function. * Uses the overloaded version from Redux-Thunk. */ -export type Dispatch = ThunkDispatch<State, void, Action<any>>; +export type Dispatch = ThunkDispatch<RootState, void, Action<any>>; /** * The type of the redux-thunk getState function. */ -export type GetState = () => State; +export type GetState = () => RootState; /** * The type of promissory actions used in the project. * Returns a promise, no extra argument, uses the global state. */ -export type Thunk = ThunkAction<Promise<any>, State, void, Action>; +export type Thunk = ThunkAction<Promise<any>, RootState, void, Action>; From b7a3e5613b5167c7aea247ca47d2f9e142e80ed0 Mon Sep 17 00:00:00 2001 From: ChrisMart21 <ChrisMart21@github.com> Date: Mon, 12 Aug 2024 17:49:58 -0700 Subject: [PATCH 41/50] ReEnable Immutable check --- src/client/app/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/app/store.ts b/src/client/app/store.ts index 3a1cce927..10d7f9a2d 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -13,7 +13,7 @@ import { Dispatch } from './types/redux/actions'; export const store = configureStore({ reducer: rootReducer, middleware: getDefaultMiddleware => getDefaultMiddleware({ - immutableCheck: false, + // immutableCheck: false, serializableCheck: false }).prepend(listenerMiddleware.middleware) .concat(baseApi.middleware), From ae28cdab5480d51fce37c0b5dad7678f9e997209 Mon Sep 17 00:00:00 2001 From: ChrisMart21 <ChrisMart21@github.com> Date: Mon, 12 Aug 2024 20:37:26 -0700 Subject: [PATCH 42/50] Bug Fix Revert Change. --- src/client/app/components/maps/MapCalibrationComponent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/app/components/maps/MapCalibrationComponent.tsx b/src/client/app/components/maps/MapCalibrationComponent.tsx index 17c6adf6a..b6d2d42f2 100644 --- a/src/client/app/components/maps/MapCalibrationComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationComponent.tsx @@ -10,7 +10,6 @@ import { CalibrationModeTypes } from '../../types/redux/map'; import MapCalibrationChartDisplayComponent from './MapCalibrationChartDisplayComponent'; import MapCalibrationInfoDisplayComponent from './MapCalibrationInfoDisplayComponent'; import MapCalibrationInitiateComponent from './MapCalibrationInitiateComponent'; -import { selectMapById } from '../../redux/api/mapsApi'; /** * @returns Calibration Component corresponding to current step invloved @@ -18,9 +17,10 @@ import { selectMapById } from '../../redux/api/mapsApi'; export const MapCalibrationComponent = () => { const mapToCalibrate = useAppSelector(localEditsSlice.selectors.selectCalibrationMapId); const calibrationMode = useAppSelector(state => { - const data = selectMapById(state, mapToCalibrate); + const data = localEditsSlice.selectors.selectLocalEdit(state, mapToCalibrate); return data?.calibrationMode ?? CalibrationModeTypes.unavailable; }); + console.log(calibrationMode); if (calibrationMode === CalibrationModeTypes.initiate) { return ( <div className='container-fluid'> From 67d578c1f2ff55d6ade58f71d7391590e98a7d8b Mon Sep 17 00:00:00 2001 From: ChrisMart21 <ChrisMart21@github.com> Date: Mon, 12 Aug 2024 21:55:16 -0700 Subject: [PATCH 43/50] Hot Fix Changes. --- .../maps/MapCalibrationChartDisplayComponent.tsx | 3 +-- .../maps/MapCalibrationInfoDisplayComponent.tsx | 6 ++++-- .../maps/MapCalibrationInitiateComponent.tsx | 3 ++- src/client/app/redux/api/authApi.ts | 2 +- src/client/app/redux/api/mapsApi.ts | 5 +++-- src/client/app/redux/slices/localEditsSlice.ts | 10 +++++----- 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx b/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx index fa415b16d..201a21459 100644 --- a/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx @@ -11,7 +11,6 @@ import { localEditsSlice } from '../../redux/slices/localEditsSlice'; import Locales from '../../types/locales'; import { CalibrationSettings } from '../../types/redux/map'; import { Dimensions, normalizeImageDimensions } from '../../utils/calibration'; -import { selectMapById } from '../../redux/api/mapsApi'; /** * @returns TODO DO ME @@ -22,7 +21,7 @@ export default function MapCalibrationChartDisplayContainer() { const y: number[] = []; const texts: string[] = []; const currentLanguange = useAppSelector(selectSelectedLanguage); - const map = useAppSelector(state => selectMapById(state, state.localEdits.calibratingMap)); + const map = useAppSelector(state => localEditsSlice.selectors.selectLocalEdit(state, localEditsSlice.selectors.selectCalibrationMapId(state))); const settings = useAppSelector(state => state.localEdits.calibrationSettings); const points = map.calibrationSet; diff --git a/src/client/app/components/maps/MapCalibrationInfoDisplayComponent.tsx b/src/client/app/components/maps/MapCalibrationInfoDisplayComponent.tsx index 359d96459..bff7993d6 100644 --- a/src/client/app/components/maps/MapCalibrationInfoDisplayComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationInfoDisplayComponent.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { ChangeEvent, FormEvent } from 'react'; import { FormattedMessage } from 'react-intl'; import { logsApi } from '../../redux/api/logApi'; -import { mapsApi, selectMapById } from '../../redux/api/mapsApi'; +import { mapsApi } from '../../redux/api/mapsApi'; import { useTranslate } from '../../redux/componentHooks'; import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; import { localEditsSlice } from '../../redux/slices/localEditsSlice'; @@ -23,7 +23,7 @@ export default function MapCalibrationInfoDisplayComponent() { const [logToServer] = logsApi.useLogToServerMutation(); const [value, setValue] = React.useState<string>(''); const showGrid = useAppSelector(state => state.localEdits.calibrationSettings.showGrid); - const mapData = useAppSelector(state => selectMapById(state, state.localEdits.calibratingMap)); + const mapData = useAppSelector(state => localEditsSlice.selectors.selectLocalEdit(state, state.localEdits.calibratingMap)); const resultDisplay = (mapData.calibrationResult) ? `x: ${mapData.calibrationResult.maxError.x}%, y: ${mapData.calibrationResult.maxError.y}%` : translate('need.more.points'); @@ -64,6 +64,8 @@ export default function MapCalibrationInfoDisplayComponent() { }; const handleChanges = () => { + console.log('MapID: ', mapData.id); + if (mapData.id < 0) { createNewMap(mapData); } else { diff --git a/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx b/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx index 8c99655cf..c0e26acd8 100644 --- a/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx @@ -45,7 +45,8 @@ export default function MapCalibrationInitiateComponent() { const [mapName, setMapName] = React.useState<string>(''); const [angle, setAngle] = React.useState<string>(''); const fileRef = React.useRef<HTMLInputElement>(null); - const mapData = useAppSelector(state => selectMapById(state, selectSelectedMap(state))); + const mapData = useAppSelector(state => localEditsSlice.selectors.selectLocalEdit(state, localEditsSlice.selectors.selectCalibrationMapId(state))); + console.log('EmpyMapData>: ', mapData); const notify = (key: 'map.bad.number' | 'map.bad.digita' | 'map.bad.digitb' | 'map.bad.load' | 'map.bad.name') => { showErrorNotification(translate(key)); diff --git a/src/client/app/redux/api/authApi.ts b/src/client/app/redux/api/authApi.ts index 9bff1db07..d9186f41a 100644 --- a/src/client/app/redux/api/authApi.ts +++ b/src/client/app/redux/api/authApi.ts @@ -55,4 +55,4 @@ export const authApi = baseApi.injectEndpoints({ }); // Poll interval in milliseconds (1 minute) -export const authPollInterval = 60000; \ No newline at end of file +export const authPollInterval = 1000 * 60 * 60; \ No newline at end of file diff --git a/src/client/app/redux/api/mapsApi.ts b/src/client/app/redux/api/mapsApi.ts index 93a82d1eb..068142dcc 100644 --- a/src/client/app/redux/api/mapsApi.ts +++ b/src/client/app/redux/api/mapsApi.ts @@ -55,12 +55,13 @@ export const mapsApi = baseApi.injectEndpoints({ opposite: (map.calibrationResult) ? map.calibrationResult.opposite : undefined } }), - onQueryStarted: (map, api) => { + onQueryStarted: async (map, api) => { api.queryFulfilled // TODO Serverlogs migrate to rtk Query to drop axios? // Requires dispatch so inconvenient - .then(() => { + .then(e => { if (map.calibrationResult) { + const { data } = e; // logToServer('info', 'New calibrated map uploaded to database'); showSuccessNotification(translate('upload.new.map.with.calibration')); } else { diff --git a/src/client/app/redux/slices/localEditsSlice.ts b/src/client/app/redux/slices/localEditsSlice.ts index c9e9002c1..78023c57b 100644 --- a/src/client/app/redux/slices/localEditsSlice.ts +++ b/src/client/app/redux/slices/localEditsSlice.ts @@ -25,7 +25,7 @@ export const localEditsSlice = createThunkSlice({ state.newMapIdCounter++; }), toggleMapShowGrid: create.reducer<void>(state => { - state.calibrationSettings.showGrid; + state.calibrationSettings.showGrid = !state.calibrationSettings.showGrid; }), setOneEdit: create.reducer<MapMetadata>((state, { payload }) => { localEditAdapter.setOne(state.mapEdits, payload); @@ -41,10 +41,10 @@ export const localEditsSlice = createThunkSlice({ }); }), createNewMap: create.reducer(state => { - state.newMapIdCounter++; + state.newMapIdCounter = state.newMapIdCounter + 1; const temporaryID = state.newMapIdCounter * -1; - localEditAdapter.setOne(state.mapEdits, { ...emtpyMapMetadata, id: temporaryID }); state.calibratingMap = temporaryID; + localEditAdapter.setOne(state.mapEdits, { ...emtpyMapMetadata, id: temporaryID }); }), offerCurrentGPS: create.reducer<GPSPoint>((state, { payload }) => { // Stripped offerCurrentGPS thunk into a single reducer for simplicity. The only missing functionality are the serverlogs @@ -78,8 +78,8 @@ export const localEditsSlice = createThunkSlice({ } } // TODO VERIFY - const xValue = eligiblePoints[0]?.x as number; - const yValue = eligiblePoints[0]?.y as number; + const xValue = eligiblePoints[0].x as number; + const yValue = eligiblePoints[0].y as number; const clickedPoint: CartesianPoint = { x: Number(xValue.toFixed(6)), y: Number(yValue.toFixed(6)) From 66ea48d526f46b91d1eb93120193f6c305db00cd Mon Sep 17 00:00:00 2001 From: ChrisMart21 <ChrisMart21@github.com> Date: Mon, 12 Aug 2024 21:58:46 -0700 Subject: [PATCH 44/50] Fix Imports --- .../app/components/maps/MapCalibrationInitiateComponent.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx b/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx index c0e26acd8..dd6f0ba41 100644 --- a/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx @@ -5,12 +5,10 @@ import * as React from 'react'; import { ChangeEvent } from 'react'; import { FormattedMessage } from 'react-intl'; -import { localEditsSlice } from '../../redux/slices/localEditsSlice'; import { logsApi } from '../../redux/api/logApi'; -import { selectMapById } from '../../redux/api/mapsApi'; import { useTranslate } from '../../redux/componentHooks'; import { useAppDispatch, useAppSelector } from '../../redux/reduxHooks'; -import { selectSelectedMap } from '../../redux/slices/graphSlice'; +import { localEditsSlice } from '../../redux/slices/localEditsSlice'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; import { showErrorNotification } from '../../utils/notifications'; From 095def8ebbfd70c33bbda724af36aa7319d558a9 Mon Sep 17 00:00:00 2001 From: ChrisMart21 <ChrisMart21@github.com> Date: Tue, 13 Aug 2024 09:08:52 -0700 Subject: [PATCH 45/50] MapChartSelect Crash Fix --- src/client/app/components/MapChartSelectComponent.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/client/app/components/MapChartSelectComponent.tsx b/src/client/app/components/MapChartSelectComponent.tsx index 4504c1847..9c1aa0b47 100644 --- a/src/client/app/components/MapChartSelectComponent.tsx +++ b/src/client/app/components/MapChartSelectComponent.tsx @@ -34,11 +34,6 @@ export default function MapChartSelectComponent() { const selectedMapData = useAppSelector(state => selectMapById(state, selectSelectedMap(state))); - const selectedMap = { - label: selectedMapData.name, - value: selectedMapData.id - }; - //useIntl instead of injectIntl and WrappedComponentProps const intl = useIntl(); @@ -51,7 +46,7 @@ export default function MapChartSelectComponent() { <div style={divBottomPadding}> <SingleSelectComponent options={sortedMaps} - selectedOption={(selectedMap.value === 0) ? undefined : selectedMap} + selectedOption={selectedMapData ? { label: selectedMapData.name, value: selectedMapData.id } : undefined} placeholder={intl.formatMessage(messages.selectMap)} onValueChange={selected => dispatch(updateSelectedMaps(selected.value))} /> From 8a535056d7b22f0511a9896d8307c516b124ead5 Mon Sep 17 00:00:00 2001 From: ChrisMart21 <ChrisMart21@github.com> Date: Tue, 13 Aug 2024 09:09:18 -0700 Subject: [PATCH 46/50] Login Invalidation updated. --- src/client/app/redux/api/authApi.ts | 4 ++-- src/client/app/redux/api/mapsApi.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/client/app/redux/api/authApi.ts b/src/client/app/redux/api/authApi.ts index d9186f41a..5d6ba6792 100644 --- a/src/client/app/redux/api/authApi.ts +++ b/src/client/app/redux/api/authApi.ts @@ -23,7 +23,7 @@ export const authApi = baseApi.injectEndpoints({ // next time the corresponding endpoint is queried, cache will be ignored and overwritten by a fresh query. // in this case, a user logged in which means that some info for ADMIN meters groups etc. // invalidate forces a refetch to any subscribed components or the next query. - invalidatesTags: ['MeterData', 'GroupData'] + invalidatesTags: ['MeterData', 'GroupData', 'MapsData'] }), verifyToken: builder.mutation<{ success: boolean }, string>({ query: token => ({ @@ -49,7 +49,7 @@ export const authApi = baseApi.injectEndpoints({ dispatch(currentUserSlice.actions.clearCurrentUser()); return { data: null }; }, - invalidatesTags: ['MeterData', 'GroupData'] + invalidatesTags: ['MeterData', 'GroupData', 'MapsData'] }) }) }); diff --git a/src/client/app/redux/api/mapsApi.ts b/src/client/app/redux/api/mapsApi.ts index 068142dcc..62e5325fb 100644 --- a/src/client/app/redux/api/mapsApi.ts +++ b/src/client/app/redux/api/mapsApi.ts @@ -59,9 +59,8 @@ export const mapsApi = baseApi.injectEndpoints({ api.queryFulfilled // TODO Serverlogs migrate to rtk Query to drop axios? // Requires dispatch so inconvenient - .then(e => { + .then(() => { if (map.calibrationResult) { - const { data } = e; // logToServer('info', 'New calibrated map uploaded to database'); showSuccessNotification(translate('upload.new.map.with.calibration')); } else { From b71527ade60c38845b7da7d48d776960aa0d6289 Mon Sep 17 00:00:00 2001 From: ChrisMart21 <ChrisMart21@github.com> Date: Tue, 13 Aug 2024 12:40:25 -0700 Subject: [PATCH 47/50] Cleanup Legacy Redux where applicable - Legacy Actions/Creators deleted, - unused actions deleted - unused types deleted - maps files purged. - comments. --- .../app/components/BarChartComponent.tsx | 2 +- .../app/components/DateRangeComponent.tsx | 7 +- .../app/components/HeaderButtonsComponent.tsx | 2 +- .../app/components/HistoryComponent.tsx | 2 +- .../app/components/LineChartComponent.tsx | 2 +- .../app/components/PlotNavComponent.tsx | 2 +- .../components/router/GraphLinkComponent.tsx | 2 +- src/client/app/redux/actions/conversions.ts | 135 ------- src/client/app/redux/actions/extraActions.ts | 14 - src/client/app/redux/actions/logs.ts | 2 + src/client/app/redux/actions/map.ts | 338 ------------------ src/client/app/redux/listenerMiddleware.ts | 2 +- .../middleware/graphHistoryMiddleware.ts | 3 +- src/client/app/redux/reducers/maps.ts | 246 ------------- src/client/app/redux/slices/appStateSlice.ts | 2 +- src/client/app/redux/slices/graphSlice.ts | 18 +- .../app/redux/slices/localEditsSlice.ts | 5 +- src/client/app/store.ts | 11 +- src/client/app/types/redux/actions.ts | 49 --- src/client/app/types/redux/map.ts | 102 +----- src/client/app/utils/api/LogsApi.ts | 2 + src/client/app/utils/api/MapsApi.ts | 40 --- src/client/app/utils/api/index.ts | 3 - src/client/app/utils/calibration.ts | 3 +- 24 files changed, 42 insertions(+), 952 deletions(-) delete mode 100644 src/client/app/redux/actions/conversions.ts delete mode 100644 src/client/app/redux/actions/extraActions.ts delete mode 100644 src/client/app/redux/actions/map.ts delete mode 100644 src/client/app/redux/reducers/maps.ts delete mode 100644 src/client/app/types/redux/actions.ts delete mode 100644 src/client/app/utils/api/MapsApi.ts diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index 755509308..02379d0f4 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -8,7 +8,7 @@ import { PlotRelayoutEvent } from 'plotly.js'; import * as React from 'react'; import Plot from 'react-plotly.js'; import { TimeInterval } from '../../../common/TimeInterval'; -import { updateSliderRange } from '../redux/actions/extraActions'; +import { updateSliderRange } from '../redux/slices/graphSlice'; import { readingsApi, stableEmptyBarReadings } from '../redux/api/readingsApi'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectPlotlyBarDataFromResult, selectPlotlyBarDeps } from '../redux/selectors/barChartSelectors'; diff --git a/src/client/app/components/DateRangeComponent.tsx b/src/client/app/components/DateRangeComponent.tsx index 22f7846ed..e4727abaf 100644 --- a/src/client/app/components/DateRangeComponent.tsx +++ b/src/client/app/components/DateRangeComponent.tsx @@ -9,9 +9,8 @@ import * as React from 'react'; import 'react-calendar/dist/Calendar.css'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; -import { changeSliderRange, selectQueryTimeInterval, updateTimeInterval, selectChartToRender} from '../redux/slices/graphSlice'; +import { changeSliderRange, selectQueryTimeInterval, updateTimeInterval, selectChartToRender } from '../redux/slices/graphSlice'; import '../styles/DateRangeCustom.css'; -import { Dispatch } from '../types/redux/actions'; import { dateRangeToTimeInterval, timeIntervalToDateRange } from '../utils/dateRangeCompatibility'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; @@ -22,11 +21,11 @@ import { ChartTypes } from '../types/redux/graph'; * @returns Date Range Calendar Picker */ export default function DateRangeComponent() { - const dispatch: Dispatch = useAppDispatch(); + const dispatch = useAppDispatch(); const queryTimeInterval = useAppSelector(selectQueryTimeInterval); const locale = useAppSelector(selectSelectedLanguage); const chartType = useAppSelector(selectChartToRender); - const datePickerVisible = chartType !== ChartTypes.compare; + const datePickerVisible = chartType !== ChartTypes.compare; const handleChange = (value: Value) => { dispatch(updateTimeInterval(dateRangeToTimeInterval(value))); diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index 39fda6430..03fe81fe7 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -8,7 +8,6 @@ import { FormattedMessage } from 'react-intl'; import { Link, useLocation } from 'react-router-dom'; import { DropdownItem, DropdownMenu, DropdownToggle, Nav, NavLink, Navbar, UncontrolledDropdown } from 'reactstrap'; import TooltipHelpComponent from '../components/TooltipHelpComponent'; -import { clearGraphHistory } from '../redux/actions/extraActions'; import { authApi } from '../redux/api/authApi'; import { selectOEDVersion } from '../redux/api/versionApi'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; @@ -19,6 +18,7 @@ import { UserRole } from '../types/items'; import translate from '../utils/translate'; import LanguageSelectorComponent from './LanguageSelectorComponent'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import { clearGraphHistory } from '../redux/slices/graphSlice'; /** * React Component that defines the header buttons at the top of a page diff --git a/src/client/app/components/HistoryComponent.tsx b/src/client/app/components/HistoryComponent.tsx index 3af439a63..7534ca6f2 100644 --- a/src/client/app/components/HistoryComponent.tsx +++ b/src/client/app/components/HistoryComponent.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectForwardHistory, selectPrevHistory } from '../redux/slices/graphSlice'; -import { historyStepBack, historyStepForward } from '../redux/actions/extraActions'; +import { historyStepBack, historyStepForward } from '../redux/slices/graphSlice'; import TooltipMarkerComponent from './TooltipMarkerComponent'; /** diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index f71058629..ad9a9fc47 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -8,7 +8,7 @@ import { PlotRelayoutEvent } from 'plotly.js'; import * as React from 'react'; import Plot from 'react-plotly.js'; import { TimeInterval } from '../../../common/TimeInterval'; -import { updateSliderRange } from '../redux/actions/extraActions'; +import { updateSliderRange } from '../redux/slices/graphSlice'; import { readingsApi, stableEmptyLineReadings } from '../redux/api/readingsApi'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectLineChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; diff --git a/src/client/app/components/PlotNavComponent.tsx b/src/client/app/components/PlotNavComponent.tsx index 21e28cd60..bd7a07ddd 100644 --- a/src/client/app/components/PlotNavComponent.tsx +++ b/src/client/app/components/PlotNavComponent.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { TimeInterval } from '../../../common/TimeInterval'; -import { clearGraphHistory } from '../redux/actions/extraActions'; +import { clearGraphHistory } from '../redux/slices/graphSlice'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectAnythingFetching } from '../redux/selectors/apiSelectors'; import { diff --git a/src/client/app/components/router/GraphLinkComponent.tsx b/src/client/app/components/router/GraphLinkComponent.tsx index d60ccb312..4efa2811d 100644 --- a/src/client/app/components/router/GraphLinkComponent.tsx +++ b/src/client/app/components/router/GraphLinkComponent.tsx @@ -6,8 +6,8 @@ import * as React from 'react'; import { Navigate, useSearchParams } from 'react-router-dom'; import { useWaitForInit } from '../../redux/componentHooks'; import { useAppDispatch } from '../../redux/reduxHooks'; -import { processGraphLink } from '../../redux/actions/extraActions'; import InitializingComponent from '../router/InitializingComponent'; +import { processGraphLink } from '../../redux/slices/graphSlice'; export const GraphLink = () => { const dispatch = useAppDispatch(); diff --git a/src/client/app/redux/actions/conversions.ts b/src/client/app/redux/actions/conversions.ts deleted file mode 100644 index 782cb48f6..000000000 --- a/src/client/app/redux/actions/conversions.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public -* License, v. 2.0. If a copy of the MPL was not distributed with this -* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/* eslint-disable jsdoc/check-param-names */ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -// @ts-nocheck -/* eslint-disable jsdoc/require-param */ - -import { Thunk, Dispatch, GetState } from '../../types/redux/actions'; -import { showSuccessNotification, showErrorNotification } from '../../utils/notifications'; -import translate from '../../utils/translate'; -import * as t from '../../types/redux/conversions'; -import { conversionsApi } from '../../utils/api'; -import { updateCikAndDBViewsIfNeeded } from './admin'; -import { conversionsSlice } from '../reducers/conversions'; - - -export function fetchConversionsDetails(): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - // ensure a fetch is not currently happening - if (!getState().conversions.isFetching) { - // set isFetching to true - dispatch(conversionsSlice.actions.requestConversionsDetails()); - // attempt to retrieve conversions details from database - const conversions = await conversionsApi.getConversionsDetails(); - // update the state with the conversions details and set isFetching to false - dispatch(conversionsSlice.actions.receiveConversionsDetails(conversions)); - // If this is the first fetch, inform the store that the first fetch has been made - if (!getState().conversions.hasBeenFetchedOnce) { - dispatch(conversionsSlice.actions.conversionsFetchedOnce()); - } - } - }; -} - - -/** - * Fetch the conversions details from the database if they have not already been fetched once - */ -export function fetchConversionsDetailsIfNeeded(): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - // If conversions have not been fetched once, return the fetchConversionDetails function - if (!getState().conversions.hasBeenFetchedOnce) { - return dispatch(fetchConversionsDetails()); - } - // If conversions have already been fetched, return a resolved promise - return Promise.resolve(); - }; -} - -export function submitEditedConversion(editedConversion: t.ConversionData, shouldRedoCik: boolean): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - // check if conversionData is already submitting (indexOf returns -1 if item does not exist in array) - - // Search the array of ConversionData in submitting for an object with source/destination ids matching that editedConversion - const conversionDataIndex = getState().conversions.submitting.findIndex(conversionData => (( - conversionData.sourceId === editedConversion.sourceId) && - conversionData.destinationId === editedConversion.destinationId)); - - // If the editedConversion is not already being submitted - if (conversionDataIndex === -1) { - // Inform the store we are about to edit the passed in conversion - // Pushes edited conversionData to submit onto the submitting state array - dispatch(conversionsSlice.actions.submitEditedConversion(editedConversion)); - // Attempt to edit the conversion in the database - try { - // posts the edited conversionData to the conversions API - await conversionsApi.edit(editedConversion); - dispatch(updateCikAndDBViewsIfNeeded(shouldRedoCik, false)); - // Update the store with our new edits - dispatch(conversionsSlice.actions.confirmEditedConversion(editedConversion)); - // Success! - showSuccessNotification(translate('conversion.successfully.edited.conversion')); - } catch (err) { - // Failure! ): - showErrorNotification(translate('conversion.failed.to.edit.conversion') + ' "' + err.response.data as string + '"'); - } - // Clear conversionData object from submitting state array - dispatch(conversionsSlice.actions.deleteSubmittedConversion(editedConversion)); - } - }; -} - -// Add conversion to database -export function addConversion(conversion: t.ConversionData): Dispatch { - return async (dispatch: Dispatch) => { - try { - // Attempt to add conversion to database - await conversionsApi.addConversion(conversion); - // Adding a new conversion only affects the Cik table - dispatch(updateCikAndDBViewsIfNeeded(true, false)); - // Update the conversions state from the database on a successful call - // In the future, getting rid of this database fetch and updating the store on a successful API call would make the page faster - // However, since the database currently assigns the id to the ConversionData we fetch from DB. - dispatch(fetchConversionsDetails()); - showSuccessNotification(translate('conversion.successfully.create.conversion')); - } catch (err) { - showErrorNotification(translate('conversion.failed.to.create.conversion') + ' "' + err.response.data as string + '"'); - } - }; -} - -// Delete conversion from database -export function deleteConversion(conversion: t.ConversionData): (dispatch: Dispatch, getState: GetState) => Promise<void> { - return async (dispatch: Dispatch, getState: GetState) => { - // Ensure the conversion is not already being worked on - // Search the array of ConversionData in submitting for an object with source/destination ids matching that conversion - const conversionDataIndex = getState().conversions.submitting.findIndex(conversionData => (( - conversionData.sourceId === conversion.sourceId) && - conversionData.destinationId === conversion.destinationId)); - - // If the conversion is not already being worked on - if (conversionDataIndex === -1) { - // Inform the store we are about to work on this conversion - // Update the submitting state array - dispatch(conversionsSlice.actions.submitEditedConversion(conversion)); - try { - // Attempt to delete the conversion from the database - await conversionsApi.delete(conversion); - // Deleting a conversion only affects the Cik table - dispatch(updateCikAndDBViewsIfNeeded(true, false)); - // Delete was successful - // Update the store to match - dispatch(conversionsSlice.actions.confirmEditedConversion(conversion)); - showSuccessNotification(translate('conversion.successfully.delete.conversion')); - } catch (err) { - showErrorNotification(translate('conversion.failed.to.delete.conversion') + ' "' + err.response.data as string + '"'); - } - // Inform the store we are done working with the conversion - dispatch(conversionsSlice.actions.deleteSubmittedConversion(conversion)); - } - }; -} diff --git a/src/client/app/redux/actions/extraActions.ts b/src/client/app/redux/actions/extraActions.ts deleted file mode 100644 index 5bac9de82..000000000 --- a/src/client/app/redux/actions/extraActions.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { createAction } from '@reduxjs/toolkit'; -import { GraphState } from 'types/redux/graph'; -import { TimeInterval } from '../../../../common/TimeInterval'; - -export const historyStepBack = createAction('graph/historyStepBack'); -export const historyStepForward = createAction('graph/historyStepForward'); -export const updateHistory = createAction<GraphState>('graph/updateHistory'); -export const processGraphLink = createAction<URLSearchParams>('graph/graphLink'); -export const clearGraphHistory = createAction('graph/clearHistory'); -export const updateSliderRange = createAction<TimeInterval>('graph/UpdateSliderRange'); diff --git a/src/client/app/redux/actions/logs.ts b/src/client/app/redux/actions/logs.ts index 318becdbe..4c366f29a 100644 --- a/src/client/app/redux/actions/logs.ts +++ b/src/client/app/redux/actions/logs.ts @@ -11,6 +11,8 @@ import { logsApi } from '../../utils/api'; * @param error An optional error object to provide a stacktrace * @param skipMail Don't e-mail this message even if we would normally emit an e-mail for this level. * @returns logs to server based on level + * TODO migrate to using RTKQuery for logging. + * This will require logging to be initiated via dispatch, which differs, and conflicts with current implementation and usage. */ export function logToServer(level: string, message: string, error?: Error, skipMail?: boolean) { const log: LogData = { diff --git a/src/client/app/redux/actions/map.ts b/src/client/app/redux/actions/map.ts deleted file mode 100644 index ae5e0a5d3..000000000 --- a/src/client/app/redux/actions/map.ts +++ /dev/null @@ -1,338 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -// @ts-ignore -// @ts-nocheck -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// TODO: Migrate to RTK - -import * as moment from 'moment'; -import { ActionType, Dispatch, GetState, Thunk } from '../../types/redux/actions'; -import * as t from '../../types/redux/map'; -import { CalibrationModeTypes, MapData, MapMetadata } from '../../types/redux/map'; -import ApiBackend from '../../utils/api/ApiBackend'; -import MapsApi from '../../utils/api/MapsApi'; -import { calibrate, CalibratedPoint, CalibrationResult, CartesianPoint, GPSPoint } from '../../utils/calibration'; -import { browserHistory } from '../../utils/history'; -import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; -import translate from '../../utils/translate'; -import { logToServer } from './logs'; -import { RootState } from 'store'; - -const mapsApi = new MapsApi(new ApiBackend()); - -function requestMapsDetails(): t.RequestMapsDetailsAction { - return { type: ActionType.RequestMapsDetails }; -} - -function receiveMapsDetails(data: MapData[]): t.ReceiveMapsDetailsAction { - return { type: ActionType.ReceiveMapsDetails, data }; -} - -function submitMapEdits(mapID: number): t.SubmitEditedMapAction { - return { type: ActionType.SubmitEditedMap, mapID }; -} - -function confirmMapEdits(mapID: number): t.ConfirmEditedMapAction { - return { type: ActionType.ConfirmEditedMap, mapID }; -} - -export function fetchMapsDetails(): Thunk { - return async (dispatch: Dispatch) => { - dispatch(requestMapsDetails()); - try { - const mapsDetails = await mapsApi.details(); - dispatch(receiveMapsDetails(mapsDetails)); - } catch (error) { - showErrorNotification(translate('failed.to.fetch.maps')); - } - }; -} - -export function editMapDetails(map: MapMetadata): t.EditMapDetailsAction { - return { type: ActionType.EditMapDetails, map }; -} - -function incrementCounter(): t.IncrementCounterAction { - return { type: ActionType.IncrementCounter }; -} - -export function setNewMap(): Thunk { - return async (dispatch: Dispatch) => { - dispatch(incrementCounter()); - dispatch((dispatch2: Dispatch, getState2: GetState) => { - const temporaryID = getState2().maps.newMapCounter * -1; - dispatch2(logToServer('info', `Set up new map, id = ${temporaryID}`)); - dispatch2(setCalibration(CalibrationModeTypes.initiate, temporaryID)); - }); - }; -} - -/** - * start a new calibration session - * @param mode calibration modes - * @param mapID id of map being calibrated - */ -export function setCalibration(mode: CalibrationModeTypes, mapID: number): Thunk { - return async (dispatch: Dispatch) => { - dispatch(prepareCalibration(mode, mapID)); - dispatch((dispatch2: Dispatch) => { - dispatch2(logToServer('info', `Start Calibration for map, id=${mapID}, mode:${mode}`)); - }); - }; -} - -export function prepareCalibration(mode: CalibrationModeTypes, mapID: number): t.SetCalibrationAction { - return { type: ActionType.SetCalibration, mode, mapID }; -} - -/** - * toggle display of grids during calibration - */ -export function changeGridDisplay(): t.ChangeGridDisplayAction { - return { type: ActionType.ChangeGridDisplay }; -} - -/** - * drop present calibration session in a traceable manner - */ -export function dropCalibration(): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - const mapToReset = getState().maps.calibratingMap; - dispatch(resetCalibration(mapToReset)); - dispatch((dispatch2: Dispatch) => { - dispatch2(logToServer('info', `reset calibration for map, id: ${mapToReset}.`)); - }); - }; -} - -function resetCalibration(mapToReset: number): t.ResetCalibrationAction { - return { type: ActionType.ResetCalibration, mapID: mapToReset }; -} - -export function updateMapSource(data: MapMetadata): t.UpdateMapSourceAction { - return { type: ActionType.UpdateMapSource, data }; -} - -export function updateMapMode(nextMode: CalibrationModeTypes): t.ChangeMapModeAction { - return { type: ActionType.UpdateCalibrationMode, nextMode }; -} - -/** - * Changes the selected map ID - * @param newSelectedMapID new map ID - */ -export function changeSelectedMap(newSelectedMapID: number): t.UpdateSelectedMapAction { - return { type: ActionType.UpdateSelectedMap, mapID: newSelectedMapID }; -} - -export function updateCurrentCartesian(currentCartesian: CartesianPoint): t.UpdateCurrentCartesianAction { - return { type: ActionType.UpdateCurrentCartesian, currentCartesian }; -} - -/** - * pair collected GPS coordinate with cartesian coordinate to form a complete data point, - * append to calibration set and trigger calibration if needed - * @param currentGPS GPS data, from user input - */ -export function offerCurrentGPS(currentGPS: GPSPoint): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - const mapID = getState().localEdits.calibratingMap; - const point = getState().localEdits.mapEdits.entities[mapID].currentPoint; - if (point && hasCartesian(point)) { - point.gps = currentGPS; - dispatch(updateCalibrationSet(point)); - dispatch(resetCurrentPoint()); - // Nesting dispatches to preserve that updateCalibrationSet() is called before calibration - dispatch(async (dispatch2: Dispatch) => { - dispatch2(logToServer('info', `gps input (lat:${currentGPS.latitude},long:${currentGPS.longitude}) - provided for cartesian point:${point.cartesian.x},${point.cartesian.y} - and added to data point`)); - if (isReadyForCalculation(getState())) { - const result = prepareDataToCalculation(getState()); - dispatch2(updateResult(result)); - dispatch2(logToServer('info', `calculation complete, maxError: x:${result.maxError.x},y:${result.maxError.y}, - origin:${result.origin.latitude},${result.origin.longitude}, opposite:${result.opposite.latitude},${result.opposite.longitude}`)); - } else { - dispatch2(logToServer('info', 'threshold not met, didn\'t trigger calibration')); - } - }); - } - return Promise.resolve(); - }; -} - -export function hasCartesian(point: CalibratedPoint) { - return point.cartesian.x !== -1 && point.cartesian.y !== -1; -} - -function updateCalibrationSet(calibratedPoint: CalibratedPoint): t.AppendCalibrationSetAction { - return { type: ActionType.AppendCalibrationSet, calibratedPoint }; -} - -/** - * use a default number as the threshold in determining if it's safe to call the calibration function - * @param state The redux state - * @returns Result of safety check - */ -function isReadyForCalculation(state: RootState): boolean { - const calibrationThreshold = 3; - // assume calibrationSet is defined, as offerCurrentGPS indicates through point that the map is defined. - return state.localEdits.mapEdits.entities[state.localEdits.calibratingMap].calibrationSet.length >= calibrationThreshold; -} - -/** - * prepare data to required formats to pass it to function calculating mapScales - * @param state The redux state - * @returns Result of map calibration - */ -function prepareDataToCalculation(state: RootState): CalibrationResult { - // TODO FIX BEFORE PR - const mapID = state.localEdits.calibratingMap; - const mp = state.localEdits.mapEdits.entities[mapID]; - // Since mp is defined above, calibrationSet is defined. - /* eslint-disable @typescript-eslint/no-non-null-assertion */ - const result = calibrate(mp); - return result; - /* eslint-enable @typescript-eslint/no-non-null-assertion */ -} - -function updateResult(result: CalibrationResult): t.UpdateCalibrationResultAction { - return { type: ActionType.UpdateCalibrationResults, result }; -} - -export function resetCurrentPoint(): t.ResetCurrentPointAction { - return { type: ActionType.ResetCurrentPoint }; -} - -export function submitEditedMaps(): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - Object.keys(getState().maps.editedMaps).forEach(mapID2Submit => { - const mapID = parseInt(mapID2Submit); - if (getState().maps.submitting.indexOf(mapID) === -1) { - dispatch(submitEditedMap(mapID)); - } - }); - }; -} - -export function submitCalibratingMap(): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - const mapID = getState().maps.calibratingMap; - if (mapID < 0) { - dispatch(submitNewMap()); - } else { - dispatch(submitEditedMap(mapID)); - } - }; -} - -/** - * submit a new map to database at the end of a calibration session - */ -export function submitNewMap(): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - const mapID = getState().maps.calibratingMap; - const map = getState().maps.editedMaps[mapID]; - try { - const acceptableMap: MapData = { - ...map, - mapSource: map.mapSource, - displayable: false, - modifiedDate: moment().toISOString(), - origin: map.calibrationResult?.origin, - opposite: map.calibrationResult?.opposite - }; - await mapsApi.create(acceptableMap); - if (map.calibrationResult) { - dispatch(logToServer('info', 'New calibrated map uploaded to database')); - showSuccessNotification(translate('upload.new.map.with.calibration')); - } else { - dispatch(logToServer('info', 'New map uploaded to database(without calibration)')); - showSuccessNotification(translate('upload.new.map.without.calibration')); - } - dispatch(confirmMapEdits(mapID)); - browserHistory.push('/maps'); - } catch (e) { - showErrorNotification(translate('failed.to.create.map')); - dispatch(logToServer('error', `failed to create map, ${e}`)); - } - }; -} - -/** - * submit changes of an existing map to database at the end of a calibration session - * @param mapID the edited map being updated at database - */ -export function submitEditedMap(mapID: number): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - const map = getState().maps.editedMaps[mapID]; - dispatch(submitMapEdits(mapID)); - try { - const acceptableMap: MapData = { - ...map, - mapSource: map.mapSource, - // As in other place, this take the time, in this case the current time, grabs the - // date and time without timezone and then set it to UTC. This allows the software - // to recreate it with the same date/time as it is on this web browser when it is - // displayed later (without the timezone shown). - // It might be better to use the server time but this is good enough. - modifiedDate: moment().format('YYYY-MM-DD HH:mm:ss') + '+00:00', - origin: map.calibrationResult?.origin, - opposite: map.calibrationResult?.opposite, - circleSize: map.circleSize - }; - await mapsApi.edit(acceptableMap); - if (map.calibrationResult) { - dispatch(logToServer('info', 'Edited map uploaded to database(newly calibrated)')); - showSuccessNotification(translate('updated.map.with.calibration')); - } else if (map.origin && map.opposite) { - dispatch(logToServer('info', 'Edited map uploaded to database(calibration not updated)')); - showSuccessNotification(translate('updated.map.without.new.calibration')); - } else { - dispatch(logToServer('info', 'Edited map uploaded to database(without calibration)')); - showSuccessNotification(translate('updated.map.without.calibration')); - } - dispatch(confirmMapEdits(mapID)); - } catch (err) { - showErrorNotification(translate('failed.to.edit.map')); - dispatch(logToServer('error', `failed to edit map, ${err}`)); - } - }; -} - -/** - * permanently remove a map - * @param mapID map to be removed - */ -export function removeMap(mapID: number): Thunk { - return async (dispatch: Dispatch) => { - try { - await mapsApi.delete(mapID); - dispatch(deleteMap(mapID)); - dispatch(logToServer('info', `Deleted map, id = ${mapID}`)); - showSuccessNotification(translate('map.is.deleted')); - browserHistory.push('/maps'); - } catch (err) { - showErrorNotification(translate('failed.to.delete.map')); - dispatch(logToServer('error', `Failed to delete map, id = ${mapID}, ${err}`)); - } - }; -} - -function deleteMap(mapID: number): t.DeleteMapAction { - return { type: ActionType.DeleteMap, mapID }; -} - -/** - * Remove all the maps in editing without submitting them - */ -export function confirmEditedMaps() { - return async (dispatch: Dispatch, getState: GetState) => { - Object.keys(getState().maps.editedMaps).forEach(mapID2Submit => { - const mapID = parseInt(mapID2Submit); - dispatch(confirmMapEdits(mapID)); - }); - }; -} diff --git a/src/client/app/redux/listenerMiddleware.ts b/src/client/app/redux/listenerMiddleware.ts index 1f2b65643..3e9bdafc0 100644 --- a/src/client/app/redux/listenerMiddleware.ts +++ b/src/client/app/redux/listenerMiddleware.ts @@ -11,7 +11,7 @@ import { unauthorizedRequestListener } from './middleware/unauthorizedAccesMiddl export const listenerMiddleware = createListenerMiddleware(); -export const startAppListening = listenerMiddleware.startListening.withTypes< RootState, AppDispatch>(); +export const startAppListening = listenerMiddleware.startListening.withTypes<RootState, AppDispatch>(); export const addAppListener = addListener.withTypes<RootState, AppDispatch>(); export type AppListener = typeof startAppListening; diff --git a/src/client/app/redux/middleware/graphHistoryMiddleware.ts b/src/client/app/redux/middleware/graphHistoryMiddleware.ts index 36ad789bf..639a89975 100644 --- a/src/client/app/redux/middleware/graphHistoryMiddleware.ts +++ b/src/client/app/redux/middleware/graphHistoryMiddleware.ts @@ -4,8 +4,7 @@ import { isAnyOf } from '@reduxjs/toolkit'; import { AppListener } from '../listenerMiddleware'; -import { graphSlice } from '../slices/graphSlice'; -import { updateHistory } from '../../redux/actions/extraActions'; +import { graphSlice, updateHistory } from '../slices/graphSlice'; export const graphHistoryListener = (startListening: AppListener) => { startListening({ diff --git a/src/client/app/redux/reducers/maps.ts b/src/client/app/redux/reducers/maps.ts deleted file mode 100644 index 3cd683941..000000000 --- a/src/client/app/redux/reducers/maps.ts +++ /dev/null @@ -1,246 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// TODO: Migrate to RTK - -import { MapMetadata, MapsAction, MapState } from '../../types/redux/map'; -import { ActionType } from '../../types/redux/actions'; -import { keyBy } from 'lodash'; -import { CalibratedPoint } from '../../utils/calibration'; - -const defaultState: MapState = { - isLoading: false, - byMapID: {}, - selectedMap: 0, - calibratingMap: 0, - editedMaps: {}, - submitting: [], - newMapCounter: 0, - calibrationSettings: { showGrid: false } -}; - -// eslint-disable-next-line jsdoc/require-jsdoc -export default function maps(state = defaultState, action: MapsAction) { - let submitting; - let editedMaps; - let byMapID; - const calibrated = state.calibratingMap; - switch (action.type) { - case ActionType.UpdateCalibrationMode: - return { - ...state, - editedMaps: { - ...state.editedMaps, - [calibrated]: { - ...state.editedMaps[calibrated], - calibrationMode: action.nextMode - } - } - }; - case ActionType.UpdateSelectedMap: - return { - ...state, - selectedMap: action.mapID - }; - case ActionType.RequestMapsDetails: - return { - ...state, - isLoading: true - }; - case ActionType.ReceiveMapsDetails: { - const data: MapMetadata[] = action.data.map(mapData => { - // parse JSON format to MapMetadata object - const parsedData = JSON.parse(JSON.stringify(mapData)); - parsedData.image = new Image(); - parsedData.image.src = parsedData.mapSource; - return parsedData; - }); - return { - ...state, - isLoading: false, - byMapID: keyBy(data, map => map.id) - }; - } - case ActionType.IncrementCounter: { - const counter = state.newMapCounter; - return { - ...state, - newMapCounter: counter + 1 - }; - } - case ActionType.SetCalibration: - byMapID = state.byMapID; - // if the map is freshly created, just add a new instance into editedMaps - if (action.mapID < 0) { - return { - ...state, - calibratingMap: action.mapID, - editedMaps: { - ...state.editedMaps, - [action.mapID]: { - id: action.mapID, - calibrationMode: action.mode - } - } - }; - } else if (state.editedMaps[action.mapID] === undefined) { - return { - ...state, - calibratingMap: action.mapID, - editedMaps: { - ...state.editedMaps, - [action.mapID]: { - // copy map from byMapID to editedMaps if there is not already a dirty map(with unsaved changes) in editedMaps - ...state.byMapID[action.mapID], - calibrationMode: action.mode - } - } - }; - } else { - return { - ...state, - calibratingMap: action.mapID, - editedMaps: { - ...state.editedMaps, - [action.mapID]: { - ...state.editedMaps[action.mapID], - calibrationMode: action.mode - } - } - }; - } - case ActionType.ChangeGridDisplay: - return { - ...state, - calibrationSettings: { - ...state.calibrationSettings, - showGrid: !state.calibrationSettings.showGrid - } - }; - case ActionType.ResetCalibration: { - editedMaps = state.editedMaps; - const mapToReset = { ...editedMaps[action.mapID] }; - delete mapToReset.currentPoint; - delete mapToReset.calibrationResult; - // TODO FIX mapsDataFetch - // delete mapToReset.calibrationSet; - return { - ...state, - editedMaps: { - ...state.editedMaps, - [calibrated]: mapToReset - } - }; - } - case ActionType.UpdateMapSource: - return { - ...state, - editedMaps: { - ...state.editedMaps, - [action.data.id]: { - ...action.data - } - } - }; - case ActionType.EditMapDetails: - editedMaps = state.editedMaps; - editedMaps[action.map.id] = action.map; - return { - ...state, - editedMaps - }; - case ActionType.SubmitEditedMap: - submitting = state.submitting; - submitting.push(action.mapID); - return { - ...state, - submitting - }; - case ActionType.ConfirmEditedMap: - submitting = state.submitting; - submitting.splice(submitting.indexOf(action.mapID), 1); - byMapID = state.byMapID; - editedMaps = state.editedMaps; - if (action.mapID > 0) { - byMapID[action.mapID] = { ...editedMaps[action.mapID] }; - } - delete editedMaps[action.mapID]; - return { - ...state, - calibratingMap: 0, - submitting, - editedMaps, - byMapID - }; - case ActionType.DeleteMap: - editedMaps = state.editedMaps; - delete editedMaps[action.mapID]; - byMapID = state.byMapID; - delete byMapID[action.mapID]; - return { - ...state, - editedMaps, - byMapID - }; - case ActionType.UpdateCurrentCartesian: { - const newDataPoint: CalibratedPoint = { - cartesian: action.currentCartesian, - gps: { longitude: -1, latitude: -1 } - }; - return { - ...state, - editedMaps: { - ...state.editedMaps, - [calibrated]: { - ...state.editedMaps[calibrated], - currentPoint: newDataPoint - } - } - }; - } - case ActionType.ResetCurrentPoint: - return { - ...state, - editedMaps: { - ...state.editedMaps, - [calibrated]: { - ...state.editedMaps[calibrated], - currentPoint: undefined - } - } - }; - case ActionType.AppendCalibrationSet: { - const originalSet = state.editedMaps[calibrated].calibrationSet; - let copiedSet; - if (originalSet) { - copiedSet = originalSet.map(point => point); - copiedSet.push(action.calibratedPoint); - } else { - copiedSet = [action.calibratedPoint]; - } - return { - ...state, - editedMaps: { - ...state.editedMaps, - [calibrated]: { - ...state.editedMaps[calibrated], - calibrationSet: copiedSet - } - } - }; - } - case ActionType.UpdateCalibrationResults: - return { - ...state, - editedMaps: { - [calibrated]: { - ...state.editedMaps[calibrated], - calibrationResult: action.result - } - } - }; - default: - return state; - } -} diff --git a/src/client/app/redux/slices/appStateSlice.ts b/src/client/app/redux/slices/appStateSlice.ts index 547249212..62aa982d2 100644 --- a/src/client/app/redux/slices/appStateSlice.ts +++ b/src/client/app/redux/slices/appStateSlice.ts @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as moment from 'moment'; -import { processGraphLink } from '../../redux/actions/extraActions'; +import { processGraphLink } from '../../redux/slices/graphSlice'; import { mapsApi } from '../../redux/api/mapsApi'; import { LanguageTypes } from '../../types/redux/i18n'; import { getToken, hasToken } from '../../utils/token'; diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index 8f83cacc2..f03ff599d 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -2,16 +2,11 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { PayloadAction, createSlice } from '@reduxjs/toolkit'; +import { PayloadAction, createAction, createSlice } from '@reduxjs/toolkit'; import { cloneDeep } from 'lodash'; import * as moment from 'moment'; import { ActionMeta } from 'react-select'; import { TimeInterval } from '../../../../common/TimeInterval'; -import { - clearGraphHistory, historyStepBack, - historyStepForward, processGraphLink, - updateHistory, updateSliderRange -} from '../../redux/actions/extraActions'; import { SelectOption } from '../../types/items'; import { ChartTypes, GraphState, LineGraphRate, MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; import { ComparePeriod, SortingOrder, calculateCompareTimeInterval, validateComparePeriod, validateSortingOrder } from '../../utils/calculateCompare'; @@ -453,3 +448,14 @@ export const { updateSelectedMaps } = graphSlice.actions; + +// Defined as External Reducers for middleware history implementation. +// Extenrally defined actions to be acted upon in 'graphSlice.extraReducers' +export const historyStepBack = createAction('graph/historyStepBack'); +export const historyStepForward = createAction('graph/historyStepForward'); +export const updateHistory = createAction<GraphState>('graph/updateHistory'); +export const processGraphLink = createAction<URLSearchParams>('graph/graphLink'); +export const clearGraphHistory = createAction('graph/clearHistory'); +export const updateSliderRange = createAction<TimeInterval>('graph/UpdateSliderRange'); + + diff --git a/src/client/app/redux/slices/localEditsSlice.ts b/src/client/app/redux/slices/localEditsSlice.ts index 78023c57b..e57c667b8 100644 --- a/src/client/app/redux/slices/localEditsSlice.ts +++ b/src/client/app/redux/slices/localEditsSlice.ts @@ -1,6 +1,5 @@ import { createEntityAdapter } from '@reduxjs/toolkit'; import { PlotMouseEvent } from 'plotly.js'; -import { hasCartesian } from '../../redux/actions/map'; import { createThunkSlice } from '../../redux/sliceCreators'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; import { calibrate, CalibratedPoint, CartesianPoint, GPSPoint } from '../../utils/calibration'; @@ -131,4 +130,8 @@ export const emtpyMapMetadata: MapMetadata = { calibrationResult: undefined, northAngle: 0, circleSize: 0 +}; + +const hasCartesian = (point: CalibratedPoint) => { + return point.cartesian.x !== -1 && point.cartesian.y !== -1; }; \ No newline at end of file diff --git a/src/client/app/store.ts b/src/client/app/store.ts index 10d7f9a2d..c59cf4828 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -8,7 +8,6 @@ import { baseApi } from './redux/api/baseApi'; import { devToolsConfig } from './redux/devToolConfig'; import { listenerMiddleware } from './redux/listenerMiddleware'; import { rootReducer } from './redux/rootReducer'; -import { Dispatch } from './types/redux/actions'; export const store = configureStore({ reducer: rootReducer, @@ -28,6 +27,10 @@ setGlobalDevModeChecks({ inputStabilityCheck: 'always', identityFunctionCheck: ' // https://react-redux.js.org/using-react-redux/usage-with-typescript#define-root-state-and-dispatch-types export type RootState = ReturnType<typeof store.getState> export type AppDispatch = typeof store.dispatch - // Adding old dispatch definition for backwards compatibility with useAppDispatch and older style thunks - // TODO eventually move away and delete Dispatch Type entirely - & Dispatch \ No newline at end of file + + +/** + * The type of the redux-thunk getState function. + * TODO verify if applicable to RTK (should be? for getState in RTKQ lifecycle thunks, and CreateAsyncThunk?) + */ +export type GetState = () => RootState; diff --git a/src/client/app/types/redux/actions.ts b/src/client/app/types/redux/actions.ts deleted file mode 100644 index d72af3afc..000000000 --- a/src/client/app/types/redux/actions.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { Action } from 'redux'; -import { ThunkAction, ThunkDispatch } from 'redux-thunk'; -import { RootState } from 'store'; - -export enum ActionType { - - UpdateUnsavedChanges = 'UPDATE_UNSAVED_CHANGES', - RemoveUnsavedChanges = 'REMOVE_UNSAVED_CHANGES', - FlipLogOutState = 'FLIP_LOG_OUT_STATE', - - UpdateCalibrationMode = 'UPDATE_MAP_MODE', - UpdateSelectedMap = 'UPDATE_SELECTED_MAPS', - UpdateMapSource = 'UPDATE_MAP_IMAGE', - ChangeGridDisplay = 'CHANGE_GRID_DISPLAY', - UpdateCurrentCartesian = 'UPDATE_CURRENT_CARTESIAN', - ResetCurrentPoint = 'RESET_CURRENT_POINT', - AppendCalibrationSet = 'APPEND_CALIBRATION_SET', - UpdateCalibrationResults = 'UPDATE_CALIBRATION_RESULTS', - RequestMapsDetails = 'REQUEST_MAP_DETAILS', - ReceiveMapsDetails = 'RECEIVE_MAP_DETAILS', - DeleteMap = 'DELETE_MAP', - EditMapDetails = 'EDIT_MAP_DETAILS', - SubmitEditedMap = 'SUBMIT_EDITED_MAP', - ConfirmEditedMap = 'CONFIRM_EDITED_MAP', - SetCalibration = 'SET_CALIBRATION', - ResetCalibration = 'RESET_CALIBRATION', - IncrementCounter = 'INCREMENT_COUNTER', -} - -/** - * The type of the redux-thunk dispatch function. - * Uses the overloaded version from Redux-Thunk. - */ -export type Dispatch = ThunkDispatch<RootState, void, Action<any>>; - -/** - * The type of the redux-thunk getState function. - */ -export type GetState = () => RootState; - -/** - * The type of promissory actions used in the project. - * Returns a promise, no extra argument, uses the global state. - */ -export type Thunk = ThunkAction<Promise<any>, RootState, void, Action>; diff --git a/src/client/app/types/redux/map.ts b/src/client/app/types/redux/map.ts index 5a07976bb..d6a89cd26 100644 --- a/src/client/app/types/redux/map.ts +++ b/src/client/app/types/redux/map.ts @@ -2,8 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { ActionType } from './actions'; -import { CalibratedPoint, CalibrationResult, CartesianPoint, GPSPoint } from '../../utils/calibration'; +import { CalibratedPoint, CalibrationResult, GPSPoint } from '../../utils/calibration'; /** * 'initiate', 'calibrate' or 'unavailable' @@ -14,106 +13,7 @@ export enum CalibrationModeTypes { unavailable = 'unavailable' } -export interface ChangeMapModeAction { - type: ActionType.UpdateCalibrationMode; - nextMode: CalibrationModeTypes; -} - -export interface RequestMapsDetailsAction { - type: ActionType.RequestMapsDetails; -} - -export interface ReceiveMapsDetailsAction { - type: ActionType.ReceiveMapsDetails; - data: MapData[]; -} - -export interface UpdateMapSourceAction { - type: ActionType.UpdateMapSource; - data: MapMetadata; -} - -export interface ChangeGridDisplayAction { - type: ActionType.ChangeGridDisplay; -} - -export interface UpdateSelectedMapAction { - type: ActionType.UpdateSelectedMap; - mapID: number; -} - -export interface UpdateCurrentCartesianAction { - type: ActionType.UpdateCurrentCartesian; - currentCartesian: CartesianPoint; -} - -export interface ResetCurrentPointAction { - type: ActionType.ResetCurrentPoint; -} - -export interface AppendCalibrationSetAction { - type: ActionType.AppendCalibrationSet; - calibratedPoint: CalibratedPoint; -} - -export interface UpdateCalibrationResultAction { - type: ActionType.UpdateCalibrationResults; - result: CalibrationResult; -} - -export interface DeleteMapAction { - type: ActionType.DeleteMap; - mapID: number; -} - -export interface EditMapDetailsAction { - type: ActionType.EditMapDetails; - map: MapMetadata; -} - -export interface SubmitEditedMapAction { - type: ActionType.SubmitEditedMap; - mapID: number; -} - -export interface ConfirmEditedMapAction { - type: ActionType.ConfirmEditedMap; - mapID: number; -} - -export interface SetCalibrationAction { - type: ActionType.SetCalibration; - mapID: number; - mode: CalibrationModeTypes; -} - -export interface IncrementCounterAction { - type: ActionType.IncrementCounter; -} - -export interface ResetCalibrationAction { - type: ActionType.ResetCalibration; - mapID: number; -} -export type MapsAction = - | ChangeMapModeAction - | UpdateSelectedMapAction - | RequestMapsDetailsAction - | ReceiveMapsDetailsAction - | UpdateMapSourceAction - | ChangeGridDisplayAction - | EditMapDetailsAction - | SubmitEditedMapAction - | ConfirmEditedMapAction - | UpdateCurrentCartesianAction - | ResetCurrentPointAction - | AppendCalibrationSetAction - | UpdateCalibrationResultAction - | SetCalibrationAction - | ResetCalibrationAction - | IncrementCounterAction - | DeleteMapAction; /** * data format stored in the database diff --git a/src/client/app/utils/api/LogsApi.ts b/src/client/app/utils/api/LogsApi.ts index d1f2a4211..36a8239b1 100644 --- a/src/client/app/utils/api/LogsApi.ts +++ b/src/client/app/utils/api/LogsApi.ts @@ -7,6 +7,8 @@ import ApiBackend from './ApiBackend'; import {LogData} from '../../types/redux/logs'; +//TODO migrate to using RTKQuery for logging. +// This will require logging to be initiated via dispatch, which differs, and conflicts with current implementation and usage. export default class LogsApi { private readonly backend: ApiBackend; diff --git a/src/client/app/utils/api/MapsApi.ts b/src/client/app/utils/api/MapsApi.ts deleted file mode 100644 index 778f0c80a..000000000 --- a/src/client/app/utils/api/MapsApi.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import ApiBackend from './ApiBackend'; -import { MapData } from '../../types/redux/map'; - -export default class MapsApi { - private readonly backend: ApiBackend; - - constructor(backend: ApiBackend) { - this.backend = backend; - } - - public async details(): Promise<MapData[]> { - return await this.backend.doGetRequest<MapData[]>('/api/maps/'); - } - - public async create(mapData: MapData): Promise<void> { - return await this.backend.doPostRequest<void>('/api/maps/create', mapData); - } - - public async edit(mapData: MapData): Promise<MapData> { - return await this.backend.doPostRequest<MapData>('/api/maps/edit', mapData); - } - - public async delete(id: number): Promise<void> { - return await this.backend.doPostRequest<void>('/api/maps/delete', { id }); - } - - public async getMapById(id: number): Promise<MapData> { - return await this.backend.doGetRequest<MapData>(`/api/maps/${id}`); - } - - public async getMapByName(name: string): Promise<MapData> { - return await this.backend.doGetRequest<MapData>('/api/maps/getByName', { 'name': name }); - } -} diff --git a/src/client/app/utils/api/index.ts b/src/client/app/utils/api/index.ts index 436a1e970..41eb6be24 100644 --- a/src/client/app/utils/api/index.ts +++ b/src/client/app/utils/api/index.ts @@ -6,19 +6,16 @@ import ApiBackend from './ApiBackend'; import UploadCSVApi from './UploadCSVApi'; -import MapsApi from './MapsApi'; import LogsApi from './LogsApi'; const apiBackend = new ApiBackend(); // All specific backends share the same ApiBackend const uploadCSVApi = new UploadCSVApi(apiBackend); -const mapsApi = new MapsApi(apiBackend); const logsApi = new LogsApi(apiBackend); export { - mapsApi, logsApi, uploadCSVApi }; diff --git a/src/client/app/utils/calibration.ts b/src/client/app/utils/calibration.ts index 63e2ca8a5..7d65d6e41 100644 --- a/src/client/app/utils/calibration.ts +++ b/src/client/app/utils/calibration.ts @@ -3,10 +3,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { showErrorNotification } from './notifications'; -import { logToServer } from '../redux/actions/logs'; import { DataType } from '../types/Datasources'; import { MapMetadata } from '../types/redux/map'; import translate from './translate'; +import { logToServer } from '../redux/actions/logs'; /** * Defines a Cartesian Point with x & y @@ -79,6 +79,7 @@ export function itemMapInfoOk(itemID: number, type: DataType, map: MapMetadata, if (map === undefined) { return false; } if ((gps === null || gps === undefined) || map.origin === undefined || map.opposite === undefined) { return false; } if (!isValidGPSInput(`${gps.latitude},${gps.longitude}`)) { + // Find way to migrate to RTKQuery logs since dispatch is required, thunks are most likely the logToServer('error', `Found invalid ${type === DataType.Meter ? 'meter' : 'group'} gps stored in database, id = ${itemID}`)(); return false; } From 18b8b6ec45c71db345ad9e18be45842c6e6e19e1 Mon Sep 17 00:00:00 2001 From: ChrisMart21 <ChrisMart21@github.com> Date: Tue, 13 Aug 2024 12:59:30 -0700 Subject: [PATCH 48/50] Fix Merge Issues / Missing MPL headers --- src/client/app/components/RouteComponent.tsx | 3 ++- src/client/app/redux/api/logApi.ts | 3 +++ src/client/app/redux/api/mapsApi.ts | 3 +++ src/client/app/redux/devToolConfig.ts | 3 +++ src/client/app/redux/entityAdapters.ts | 3 +++ src/client/app/redux/slices/localEditsSlice.ts | 4 +++- src/client/app/utils/api/index.ts | 6 +----- 7 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/client/app/components/RouteComponent.tsx b/src/client/app/components/RouteComponent.tsx index c25395c63..16d93065b 100644 --- a/src/client/app/components/RouteComponent.tsx +++ b/src/client/app/components/RouteComponent.tsx @@ -4,7 +4,6 @@ import * as React from 'react'; import { IntlProvider } from 'react-intl'; import { RouterProvider, createBrowserRouter } from 'react-router-dom'; -import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; import { useAppSelector } from '../redux/reduxHooks'; import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; import LocaleTranslationData from '../translations/data'; @@ -25,6 +24,8 @@ import { GraphLink } from './router/GraphLinkComponent'; import NotFound from './router/NotFoundOutlet'; import RoleOutlet from './router/RoleOutlet'; import UnitsDetailComponent from './unit/UnitsDetailComponent'; +import MetersCSVUploadComponent from './csv/MetersCSVUploadComponent'; +import ReadingsCSVUploadComponent from './csv/ReadingsCSVUploadComponent'; /** * @returns the router component Responsible for client side routing. diff --git a/src/client/app/redux/api/logApi.ts b/src/client/app/redux/api/logApi.ts index f601e469a..9c913ae8a 100644 --- a/src/client/app/redux/api/logApi.ts +++ b/src/client/app/redux/api/logApi.ts @@ -1,3 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { LogData } from 'types/redux/logs'; import { baseApi } from './baseApi'; diff --git a/src/client/app/redux/api/mapsApi.ts b/src/client/app/redux/api/mapsApi.ts index 62e5325fb..36eb5171b 100644 --- a/src/client/app/redux/api/mapsApi.ts +++ b/src/client/app/redux/api/mapsApi.ts @@ -1,3 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { pick } from 'lodash'; import * as moment from 'moment'; import { MapDataState, mapsAdapter, mapsInitialState } from '../../redux/entityAdapters'; diff --git a/src/client/app/redux/devToolConfig.ts b/src/client/app/redux/devToolConfig.ts index 02fe511bc..6a5d862fb 100644 --- a/src/client/app/redux/devToolConfig.ts +++ b/src/client/app/redux/devToolConfig.ts @@ -1,3 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { DevToolsEnhancerOptions } from '@reduxjs/toolkit'; import { mapsApi } from './api/mapsApi'; import { mapsAdapter, mapsInitialState } from './entityAdapters'; diff --git a/src/client/app/redux/entityAdapters.ts b/src/client/app/redux/entityAdapters.ts index 92402a2ee..582b540e5 100644 --- a/src/client/app/redux/entityAdapters.ts +++ b/src/client/app/redux/entityAdapters.ts @@ -1,3 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { EntityState, createEntityAdapter } from '@reduxjs/toolkit'; import { ConversionData } from '../types/redux/conversions'; import { GroupData } from '../types/redux/groups'; diff --git a/src/client/app/redux/slices/localEditsSlice.ts b/src/client/app/redux/slices/localEditsSlice.ts index e57c667b8..bfb4ae48b 100644 --- a/src/client/app/redux/slices/localEditsSlice.ts +++ b/src/client/app/redux/slices/localEditsSlice.ts @@ -1,3 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { createEntityAdapter } from '@reduxjs/toolkit'; import { PlotMouseEvent } from 'plotly.js'; import { createThunkSlice } from '../../redux/sliceCreators'; @@ -49,7 +52,6 @@ export const localEditsSlice = createThunkSlice({ // Stripped offerCurrentGPS thunk into a single reducer for simplicity. The only missing functionality are the serverlogs // Current axios approach doesn't require dispatch, however if moved to rtk will. thunks for this adds complexity // For simplicity, these logs can instead be tabulated in a middleware.(probably.) - // const map = localEditAdapter.getSelectors().selectById(state.mapEdits, state.calibratingMap); const map = state.mapEdits.entities[state.calibratingMap]; const point = map.currentPoint; diff --git a/src/client/app/utils/api/index.ts b/src/client/app/utils/api/index.ts index 41eb6be24..583c15999 100644 --- a/src/client/app/utils/api/index.ts +++ b/src/client/app/utils/api/index.ts @@ -5,17 +5,13 @@ */ import ApiBackend from './ApiBackend'; -import UploadCSVApi from './UploadCSVApi'; import LogsApi from './LogsApi'; const apiBackend = new ApiBackend(); // All specific backends share the same ApiBackend -const uploadCSVApi = new UploadCSVApi(apiBackend); const logsApi = new LogsApi(apiBackend); - export { - logsApi, - uploadCSVApi + logsApi }; From a8237ba46c1c4ea7f85705444e594994268cd17b Mon Sep 17 00:00:00 2001 From: ChrisMart21 <ChrisMart21@github.com> Date: Tue, 13 Aug 2024 18:28:10 -0700 Subject: [PATCH 49/50] Update Root Reducer, Sanitize Devtools --- src/client/app/redux/devToolConfig.ts | 53 ++++++++++++++++++- .../middleware/graphHistoryMiddleware.ts | 3 +- src/client/app/redux/rootReducer.ts | 23 ++++---- src/client/app/store.ts | 2 +- 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/client/app/redux/devToolConfig.ts b/src/client/app/redux/devToolConfig.ts index 6a5d862fb..a9ed53eed 100644 --- a/src/client/app/redux/devToolConfig.ts +++ b/src/client/app/redux/devToolConfig.ts @@ -2,9 +2,11 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { DevToolsEnhancerOptions } from '@reduxjs/toolkit'; -import { mapsApi } from './api/mapsApi'; +import { RootState } from 'store'; +import { mapsApi, selectAllMaps, selectMapIds } from './api/mapsApi'; import { mapsAdapter, mapsInitialState } from './entityAdapters'; +const devToolSanitizedDataMessage = 'Omitted From Devtools - Refer to devToolConfig.ts for Details'; export const devToolsConfig: DevToolsEnhancerOptions = { actionSanitizer: action => { switch (true) { @@ -12,7 +14,7 @@ export const devToolsConfig: DevToolsEnhancerOptions = { case mapsApi.endpoints.getMapDetails.matchFulfilled(action): { // omitMapSource from metaData const sanitizedMapMetadata = Object.values(action.payload.entities) - .map(data => ({ ...data, mapSource: 'Omitted From Devtools Serialization' })); + .map(data => ({ ...data, mapSource: devToolSanitizedDataMessage })); // sanitized devtool Action return { ...action, payload: { ...mapsAdapter.setAll(mapsInitialState, sanitizedMapMetadata) } }; @@ -20,6 +22,53 @@ export const devToolsConfig: DevToolsEnhancerOptions = { default: return action; } + }, + stateSanitizer: state => { + const sanitizedState = sanitizeState(state as RootState); + return sanitizedState as typeof state; + } +}; +export const sanitizeState = (state: RootState) => { + let s: RootState = state; + // if there are map entries in state, sanitize their map source. + if (selectMapIds(s).length) { + const sanitizedEntities = selectAllMaps(s).map(mapMetaData => ({ ...mapMetaData, mapSource: devToolSanitizedDataMessage })); + // recreate Sanitized Cache + const sanitizedCache = mapsAdapter.setAll(mapsAdapter.getInitialState(), sanitizedEntities); + s = { + ...s, + api: { + ...s.api, + queries: { + ...s.api.queries, + 'getMapDetails(undefined)': { + ...s.api.queries['getMapDetails(undefined)'], + data: sanitizedCache + } + } + } + } as RootState; + } + // Sanitize localEditsDevtools mapSource + if (s.localEdits.mapEdits.ids.length) { + const sanitizedEntities = mapsAdapter + .getSelectors() + .selectAll(s.localEdits.mapEdits) + .map(mapMetaData => ({ ...mapMetaData, mapSource: devToolSanitizedDataMessage })); + // recreate Sanitized Cache + const sanitizedCache = mapsAdapter.setAll(mapsAdapter.getInitialState(), sanitizedEntities); + s = { + ...s, + localEdits: { + ...s.localEdits, + mapEdits: sanitizedCache + } + } as RootState; } + // Sanitize some more ... + + + // return sanitized state, for devtools + return s; }; \ No newline at end of file diff --git a/src/client/app/redux/middleware/graphHistoryMiddleware.ts b/src/client/app/redux/middleware/graphHistoryMiddleware.ts index 639a89975..86e18e149 100644 --- a/src/client/app/redux/middleware/graphHistoryMiddleware.ts +++ b/src/client/app/redux/middleware/graphHistoryMiddleware.ts @@ -20,5 +20,6 @@ export const graphHistoryListener = (startListening: AppListener) => { }); }; -// listen to all graphSlice actions +// listen to all graphSlice actions defined in graphSlice.reducers. +// Updating state via graphsSlice.extraReducers will not trigger history middleware const isHistoryTrigger = isAnyOf(...Object.values(graphSlice.actions)); diff --git a/src/client/app/redux/rootReducer.ts b/src/client/app/redux/rootReducer.ts index 9e5f33a1c..623a37a72 100644 --- a/src/client/app/redux/rootReducer.ts +++ b/src/client/app/redux/rootReducer.ts @@ -2,22 +2,23 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { combineReducers } from 'redux'; import { baseApi } from './api/baseApi'; import { adminSlice } from './slices/adminSlice'; import { appStateSlice } from './slices/appStateSlice'; import { currentUserSlice } from './slices/currentUserSlice'; import { graphSlice } from './slices/graphSlice'; // import maps from './reducers/maps'; +import { combineSlices } from '@reduxjs/toolkit'; import { localEditsSlice } from './slices/localEditsSlice'; -export const rootReducer = combineReducers({ - appState: appStateSlice.reducer, - graph: graphSlice.reducer, - admin: adminSlice.reducer, - currentUser: currentUserSlice.reducer, - localEdits: localEditsSlice.reducer, - // RTK Query's Derived Reducers - [baseApi.reducerPath]: baseApi.reducer - // maps -}); \ No newline at end of file +// export const rootReducer = combineReducers({ +// appState: appStateSlice.reducer, +// graph: graphSlice.reducer, +// admin: adminSlice.reducer, +// currentUser: currentUserSlice.reducer, +// localEdits: localEditsSlice.reducer, +// // RTK Query's Derived Reducers +// [baseApi.reducerPath]: baseApi.reducer +// // maps +// }); +export const rootReducer = combineSlices(appStateSlice, graphSlice, adminSlice, currentUserSlice, localEditsSlice, baseApi); \ No newline at end of file diff --git a/src/client/app/store.ts b/src/client/app/store.ts index c59cf4828..239a0733d 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -13,7 +13,7 @@ export const store = configureStore({ reducer: rootReducer, middleware: getDefaultMiddleware => getDefaultMiddleware({ // immutableCheck: false, - serializableCheck: false + // serializableCheck: false }).prepend(listenerMiddleware.middleware) .concat(baseApi.middleware), devTools: devToolsConfig From 04ba6a5e753e4354a6365ef9605f7981b3143807 Mon Sep 17 00:00:00 2001 From: ChrisMart21 <ChrisMart21@github.com> Date: Tue, 13 Aug 2024 19:49:49 -0700 Subject: [PATCH 50/50] Address Unserializable Values In State/Actions. - queryTimeIntervalString - rangeSliderIntervalString - barDuration - mapsBarDuration - compareTimeIntervalString - Re-Enable DevModeChecks --- .../app/components/BarChartComponent.tsx | 5 +- .../app/components/DateRangeComponent.tsx | 4 +- .../app/components/LineChartComponent.tsx | 5 +- .../app/components/PlotNavComponent.tsx | 4 +- src/client/app/components/PlotOED.tsx | 5 +- src/client/app/components/ThreeDComponent.tsx | 5 +- .../MapCalibrationChartDisplayComponent.tsx | 22 ++++- src/client/app/redux/api/mapsApi.ts | 10 ++ src/client/app/redux/selectors/uiSelectors.ts | 5 +- src/client/app/redux/slices/graphSlice.ts | 96 +++++++++++++------ .../app/redux/slices/localEditsSlice.ts | 27 +----- src/client/app/types/redux/graph.ts | 15 +-- 12 files changed, 126 insertions(+), 77 deletions(-) diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index 02379d0f4..bcccceb67 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -115,14 +115,15 @@ export default function BarChartComponent() { const startTS = utc(e['xaxis.range[0]']); const endTS = utc(e['xaxis.range[1]']); const workingTimeInterval = new TimeInterval(startTS, endTS); - dispatch(updateSliderRange(workingTimeInterval)); + dispatch(updateSliderRange(workingTimeInterval.toString())); } else if (e['xaxis.range']) { // this case is when the slider knobs are dragged. const range = e['xaxis.range']!; const startTS = range && range[0]; const endTS = range && range[1]; - dispatch(updateSliderRange(new TimeInterval(utc(startTS), utc(endTS)))); + const interval = new TimeInterval(utc(startTS), utc(endTS)).toString(); + dispatch(updateSliderRange(interval)); } }, 500, { leading: false, trailing: true })} diff --git a/src/client/app/components/DateRangeComponent.tsx b/src/client/app/components/DateRangeComponent.tsx index e4727abaf..ceffc9e14 100644 --- a/src/client/app/components/DateRangeComponent.tsx +++ b/src/client/app/components/DateRangeComponent.tsx @@ -28,8 +28,8 @@ export default function DateRangeComponent() { const datePickerVisible = chartType !== ChartTypes.compare; const handleChange = (value: Value) => { - dispatch(updateTimeInterval(dateRangeToTimeInterval(value))); - dispatch(changeSliderRange(dateRangeToTimeInterval(value))); + dispatch(updateTimeInterval(dateRangeToTimeInterval(value).toString())); + dispatch(changeSliderRange(dateRangeToTimeInterval(value).toString())); }; diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index ad9a9fc47..2c909609e 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -101,14 +101,15 @@ export default function LineChartComponent() { const startTS = utc(e['xaxis.range[0]']); const endTS = utc(e['xaxis.range[1]']); const workingTimeInterval = new TimeInterval(startTS, endTS); - dispatch(updateSliderRange(workingTimeInterval)); + dispatch(updateSliderRange(workingTimeInterval.toString())); } else if (e['xaxis.range']) { // this case is when the slider knobs are dragged. const range = e['xaxis.range']!; const startTS = range && range[0]; const endTS = range && range[1]; - dispatch(updateSliderRange(new TimeInterval(utc(startTS), utc(endTS)))); + const interval = new TimeInterval(utc(startTS), utc(endTS)); + dispatch(updateSliderRange(interval.toString())); } }, 500, { leading: false, trailing: true }) diff --git a/src/client/app/components/PlotNavComponent.tsx b/src/client/app/components/PlotNavComponent.tsx index bd7a07ddd..0729c53b5 100644 --- a/src/client/app/components/PlotNavComponent.tsx +++ b/src/client/app/components/PlotNavComponent.tsx @@ -40,7 +40,7 @@ export const ExpandComponent = () => { const dispatch = useAppDispatch(); return ( <img src='./expand.png' style={{ height: '25px' }} - onClick={() => { dispatch(changeSliderRange(TimeInterval.unbounded())); }} + onClick={() => { dispatch(changeSliderRange(TimeInterval.unbounded().toString())); }} /> ); }; @@ -70,7 +70,7 @@ export const RefreshGraphComponent = () => { <img src='./refresh.png' style={{ height: '25px', transform: `rotate(${time}deg)`, visibility: iconVisible ? 'visible' : 'hidden' }} - onClick={() => { !somethingFetching && dispatch(updateTimeInterval(sliderInterval)); }} + onClick={() => { !somethingFetching && dispatch(updateTimeInterval(sliderInterval.toString())); }} /> ); }; diff --git a/src/client/app/components/PlotOED.tsx b/src/client/app/components/PlotOED.tsx index b460cf79a..0adbe7fdf 100644 --- a/src/client/app/components/PlotOED.tsx +++ b/src/client/app/components/PlotOED.tsx @@ -44,14 +44,15 @@ export const PlotOED = (props: OEDPlotProps) => { const startTS = moment.utc(e['xaxis.range[0]']); const endTS = moment.utc(e['xaxis.range[1]']); const workingTimeInterval = new TimeInterval(startTS, endTS); - dispatch(changeSliderRange(workingTimeInterval)); + dispatch(changeSliderRange(workingTimeInterval.toString())); } else if (e['xaxis.range']) { // this case is when the slider knobs are dragged. const range = figure.current.layout?.xaxis?.range; const startTS = range && range[0]; const endTS = range && range[1]; - dispatch(changeSliderRange(new TimeInterval(startTS, endTS))); + const interval = new TimeInterval(startTS, endTS).toString(); + dispatch(changeSliderRange(interval)); } }, 500, { leading: false, trailing: true }); diff --git a/src/client/app/components/ThreeDComponent.tsx b/src/client/app/components/ThreeDComponent.tsx index 658df7415..37305d073 100644 --- a/src/client/app/components/ThreeDComponent.tsx +++ b/src/client/app/components/ThreeDComponent.tsx @@ -11,7 +11,7 @@ import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppSelector } from '../redux/reduxHooks'; import { selectThreeDQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { selectThreeDComponentInfo } from '../redux/selectors/threeDSelectors'; -import { selectGraphState } from '../redux/slices/graphSlice'; +import { selectGraphState, selectQueryTimeInterval } from '../redux/slices/graphSlice'; import { ThreeDReading } from '../types/readings'; import { GraphState, MeterOrGroup } from '../types/redux/graph'; import { GroupDataByID } from '../types/redux/groups'; @@ -38,6 +38,7 @@ export default function ThreeDComponent() { const groupDataById = useAppSelector(selectGroupDataById); const unitDataById = useAppSelector(selectUnitDataById); const graphState = useAppSelector(selectGraphState); + const queryTimeInterval = useAppSelector(selectQueryTimeInterval); const locale = useAppSelector(selectSelectedLanguage); const { meterOrGroupID, meterOrGroupName, isAreaCompatible } = useAppSelector(selectThreeDComponentInfo); @@ -53,7 +54,7 @@ export default function ThreeDComponent() { layout = setHelpLayout(translate('select.meter.group')); } else if (graphState.areaNormalization && !isAreaCompatible) { layout = setHelpLayout(`${meterOrGroupName}${translate('threeD.area.incompatible')}`); - } else if (!isValidThreeDInterval(roundTimeIntervalForFetch(graphState.queryTimeInterval))) { + } else if (!isValidThreeDInterval(roundTimeIntervalForFetch(queryTimeInterval))) { // Not a valid time interval. ThreeD can only support up to 1 year of readings layout = setHelpLayout(translate('threeD.date.range.too.long')); } else if (!threeDData) { diff --git a/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx b/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx index 201a21459..089bc66cd 100644 --- a/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationChartDisplayComponent.tsx @@ -10,7 +10,7 @@ import { selectSelectedLanguage } from '../../redux/slices/appStateSlice'; import { localEditsSlice } from '../../redux/slices/localEditsSlice'; import Locales from '../../types/locales'; import { CalibrationSettings } from '../../types/redux/map'; -import { Dimensions, normalizeImageDimensions } from '../../utils/calibration'; +import { CartesianPoint, Dimensions, normalizeImageDimensions } from '../../utils/calibration'; /** * @returns TODO DO ME @@ -93,8 +93,26 @@ export default function MapCalibrationChartDisplayContainer() { locale: currentLanguange }} onClick={(event: PlotMouseEvent) => { + // trace 0 keeps a transparent trace of closely positioned points used for calibration(backgroundTrace), + // trace 1 keeps the data points used for calibration are automatically added to the same trace(dataPointTrace), + // event.points will include all points near a mouse click, including those in the backgroundTrace and the dataPointTrace, + // so the algorithm only looks at trace 0 since points from trace 1 are already put into the data set used for calibration. event.event.preventDefault(); - dispatch(localEditsSlice.actions.updateCurrentCartesian(event)); + const eligiblePoints = []; + for (const point of event.points) { + const traceNumber = point.curveNumber; + if (traceNumber === 0) { + eligiblePoints.push(point); + } + } + // TODO VERIFY + const xValue = eligiblePoints[0].x as number; + const yValue = eligiblePoints[0].y as number; + const clickedPoint: CartesianPoint = { + x: Number(xValue.toFixed(6)), + y: Number(yValue.toFixed(6)) + }; + dispatch(localEditsSlice.actions.updateCurrentCartesian(clickedPoint)); }} />; } diff --git a/src/client/app/redux/api/mapsApi.ts b/src/client/app/redux/api/mapsApi.ts index 36eb5171b..aa7411c9b 100644 --- a/src/client/app/redux/api/mapsApi.ts +++ b/src/client/app/redux/api/mapsApi.ts @@ -5,6 +5,7 @@ import { pick } from 'lodash'; import * as moment from 'moment'; import { MapDataState, mapsAdapter, mapsInitialState } from '../../redux/entityAdapters'; import { createAppSelector } from '../../redux/selectors/selectors'; +import { setGraphSliceState } from '../../redux/slices/graphSlice'; import { emtpyMapMetadata, localEditsSlice } from '../../redux/slices/localEditsSlice'; import { RootState } from '../../store'; import { MapData, MapMetadata } from '../../types/redux/map'; @@ -125,12 +126,21 @@ export const mapsApi = baseApi.injectEndpoints({ body: { id } }), onQueryStarted: (arg, api) => { + const s = api.getState() as RootState; api.queryFulfilled //Cleanup Local Edits if any for deleted entity .then(() => { + // set current to 0 if current selected is arg + const updatedCurrent = s.graph.current.selectedMap === arg ? { ...s.graph.current, selectedMap: 0 } : s.graph.current; + // filter entries with this id + const filteredPrev = s.graph.prev.filter(graphState => graphState.selectedMap === arg); + const filteredNext = s.graph.next.filter(graphState => graphState.selectedMap === arg); + api.dispatch(setGraphSliceState({ prev: filteredPrev, current: updatedCurrent, next: filteredNext })); api.dispatch(localEditsSlice.actions.removeOneEdit(arg)); }) .catch(); + + }, invalidatesTags: ['MapsData'] }), diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 26190d805..387a6fd4a 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -27,6 +27,7 @@ import { import { selectVisibleMetersAndGroups, selectVisibleUnitOrSuffixState } from './authVisibilitySelectors'; import { selectDefaultGraphicUnitFromEntity, selectMeterOrGroupFromEntity, selectNameFromEntity } from './entitySelectors'; import { createAppSelector } from './selectors'; +import moment from 'moment'; export const selectCurrentUnitCompatibility = createAppSelector( [ @@ -459,10 +460,10 @@ export const selectChartLink = createAppSelector( } linkText += `chartType=${current.chartToRender}`; // weeklyLink = linkText + '&serverRange=7dfp'; // dfp: days from present; - linkText += `&serverRange=${current.queryTimeInterval.toString()}`; + linkText += `&serverRange=${current.queryTimeIntervalString.toString()}`; switch (current.chartToRender) { case ChartTypes.bar: - linkText += `&barDuration=${current.barDuration.asDays()}`; + linkText += `&barDuration=${moment.duration(current.barDuration).asDays()}`; linkText += `&barStacking=${current.barStacking}`; break; case ChartTypes.line: diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index f03ff599d..973b68aa2 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { PayloadAction, createAction, createSlice } from '@reduxjs/toolkit'; +import { PayloadAction, createAction, createSelector, createSlice } from '@reduxjs/toolkit'; import { cloneDeep } from 'lodash'; import * as moment from 'moment'; import { ActionMeta } from 'react-select'; @@ -21,12 +21,12 @@ const defaultState: GraphState = { selectedAreaUnit: AreaUnitType.none, // TODO appropriate default value? selectedMap: 0, - queryTimeInterval: TimeInterval.unbounded(), - rangeSliderInterval: TimeInterval.unbounded(), - barDuration: moment.duration(4, 'weeks'), - mapsBarDuration: moment.duration(4, 'weeks'), + queryTimeIntervalString: TimeInterval.unbounded().toString(), + rangeSliderIntervalString: TimeInterval.unbounded().toString(), + barDuration: moment.duration(4, 'weeks').toISOString(), + mapsBarDuration: moment.duration(4, 'weeks').toISOString(), + compareTimeIntervalString: calculateCompareTimeInterval(ComparePeriod.Week, moment()).toString(), comparePeriod: ComparePeriod.Week, - compareTimeInterval: calculateCompareTimeInterval(ComparePeriod.Week, moment()), compareSortingOrder: SortingOrder.Descending, chartToRender: ChartTypes.line, barStacking: false, @@ -83,29 +83,29 @@ export const graphSlice = createSlice({ state.current.selectedAreaUnit = action.payload; }, updateBarDuration: (state, action: PayloadAction<moment.Duration>) => { - state.current.barDuration = action.payload; + state.current.barDuration = action.payload.toISOString(); }, updateMapsBarDuration: (state, action: PayloadAction<moment.Duration>) => { - state.current.mapsBarDuration = action.payload; + state.current.mapsBarDuration = action.payload.toISOString(); }, - updateTimeInterval: (state, action: PayloadAction<TimeInterval>) => { + updateTimeInterval: (state, action: PayloadAction<string>) => { // always update if action is bounded, else only set unbounded if current isn't already unbounded. // clearing when already unbounded should be a no-op - if (action.payload.getIsBounded() || state.current.queryTimeInterval.getIsBounded()) { - state.current.queryTimeInterval = action.payload; + if (TimeInterval.fromString(action.payload).getIsBounded() || TimeInterval.fromString(state.current.queryTimeIntervalString).getIsBounded()) { + state.current.queryTimeIntervalString = action.payload.toString(); } }, - changeSliderRange: (state, action: PayloadAction<TimeInterval>) => { - if (action.payload.getIsBounded() || state.current.rangeSliderInterval.getIsBounded()) { - state.current.rangeSliderInterval = action.payload; + changeSliderRange: (state, action: PayloadAction<string>) => { + if (TimeInterval.fromString(action.payload).getIsBounded() || TimeInterval.fromString(state.current.rangeSliderIntervalString).getIsBounded()) { + state.current.rangeSliderIntervalString = action.payload.toString(); } }, resetRangeSliderStack: state => { - state.current.rangeSliderInterval = TimeInterval.unbounded(); + state.current.rangeSliderIntervalString = TimeInterval.unbounded().toString(); }, updateComparePeriod: (state, action: PayloadAction<{ comparePeriod: ComparePeriod, currentTime: moment.Moment }>) => { state.current.comparePeriod = action.payload.comparePeriod; - state.current.compareTimeInterval = calculateCompareTimeInterval(action.payload.comparePeriod, action.payload.currentTime); + state.current.compareTimeIntervalString = calculateCompareTimeInterval(action.payload.comparePeriod, action.payload.currentTime).toString(); }, changeChartToRender: (state, action: PayloadAction<ChartTypes>) => { state.current.chartToRender = action.payload; @@ -234,8 +234,8 @@ export const graphSlice = createSlice({ } }, resetTimeInterval: state => { - if (!state.current.queryTimeInterval.equals(TimeInterval.unbounded())) { - state.current.queryTimeInterval = TimeInterval.unbounded(); + if (!TimeInterval.fromString(state.current.queryTimeIntervalString).equals(TimeInterval.unbounded())) { + state.current.queryTimeIntervalString = TimeInterval.unbounded().toString(); } }, setGraphState: (state, action: PayloadAction<GraphState>) => { @@ -247,6 +247,10 @@ export const graphSlice = createSlice({ // Current History Implementation tracks ANY action defined in 'reducers' // To update graphState without causing a history entry to be created, utilize the 'Extra Reducers' property builder + .addCase( + setGraphSliceState, + (_state, action) => action.payload + ) .addCase( updateHistory, (state, action) => { @@ -285,7 +289,7 @@ export const graphSlice = createSlice({ .addCase( updateSliderRange, (state, { payload }) => { - state.current.rangeSliderInterval = payload; + state.current.rangeSliderIntervalString = payload; } ) .addCase( @@ -304,7 +308,7 @@ export const graphSlice = createSlice({ current.selectedAreaUnit = value as AreaUnitType; break; case 'barDuration': - current.barDuration = moment.duration(parseInt(value), 'days'); + current.barDuration = moment.duration(parseInt(value), 'days').toISOString(); break; case 'barStacking': current.barStacking = value === 'true'; @@ -315,7 +319,7 @@ export const graphSlice = createSlice({ case 'comparePeriod': { current.comparePeriod = validateComparePeriod(value); - current.compareTimeInterval = calculateCompareTimeInterval(validateComparePeriod(value), moment()); + current.compareTimeIntervalString = calculateCompareTimeInterval(validateComparePeriod(value), moment()).toString(); } break; case 'compareSortingOrder': @@ -347,7 +351,7 @@ export const graphSlice = createSlice({ current.threeD.readingInterval = parseInt(value); break; case 'serverRange': - current.queryTimeInterval = TimeInterval.fromString(value); + current.queryTimeIntervalString = value; break; case 'sliderRange': // TODO omitted for now re-implement later. @@ -387,8 +391,6 @@ export const graphSlice = createSlice({ selectThreeDState: state => state.current.threeD, selectShowMinMax: state => state.current.showMinMax, selectBarStacking: state => state.current.barStacking, - selectBarWidthDays: state => state.current.barDuration, - selectMapBarWidthDays: state => state.current.mapsBarDuration, selectSelectedMap: state => state.current.selectedMap, selectAreaUnit: state => state.current.selectedAreaUnit, selectSelectedUnit: state => state.current.selectedUnit, @@ -398,17 +400,50 @@ export const graphSlice = createSlice({ selectSelectedMeters: state => state.current.selectedMeters, selectSelectedGroups: state => state.current.selectedGroups, selectSortingOrder: state => state.current.compareSortingOrder, - selectQueryTimeInterval: state => state.current.queryTimeInterval, selectThreeDMeterOrGroup: state => state.current.threeD.meterOrGroup, - selectCompareTimeInterval: state => state.current.compareTimeInterval, selectGraphAreaNormalization: state => state.current.areaNormalization, selectThreeDMeterOrGroupID: state => state.current.threeD.meterOrGroupID, selectThreeDReadingInterval: state => state.current.threeD.readingInterval, selectDefaultGraphState: () => defaultState, selectHistoryIsDirty: state => state.prev.length > 0 || state.next.length > 0, - selectSliderRangeInterval: state => state.current.rangeSliderInterval, - selectPlotlySliderMin: state => state.current.rangeSliderInterval.getStartTimestamp()?.utc().toDate().toISOString(), - selectPlotlySliderMax: state => state.current.rangeSliderInterval.getEndTimestamp()?.utc().toDate().toISOString() + selectPlotlySliderMin: state => TimeInterval.fromString(state.current.rangeSliderIntervalString).getStartTimestamp()?.utc().toDate().toISOString(), + selectPlotlySliderMax: state => TimeInterval.fromString(state.current.rangeSliderIntervalString).getEndTimestamp()?.utc().toDate().toISOString(), + selectQueryTimeIntervalString: state => state.current.queryTimeIntervalString, + selectCompareTimeIntervalString: state => state.current.compareTimeIntervalString, + selectSliderRangeIntervalString: state => state.current.rangeSliderIntervalString, + + // Memoized selector(s) becuase creating new TimeInterval.fromString(), each execution leads to unnecessary re-renders + // Avoids Saving Un-serializable objects (TimeIntervals) in store. + selectQueryTimeInterval: createSelector( + (sliceState: History<GraphState>) => sliceState.current.queryTimeIntervalString, + timeIntervalString => { + return TimeInterval.fromString(timeIntervalString); + } + ), + selectSliderRangeInterval: createSelector( + (sliceState: History<GraphState>) => sliceState.current.rangeSliderIntervalString, + timeIntervalString => { + return TimeInterval.fromString(timeIntervalString); + } + ), + selectCompareTimeInterval: createSelector( + (sliceState: History<GraphState>) => sliceState.current.compareTimeIntervalString, + timeIntervalString => { + return TimeInterval.fromString(timeIntervalString); + } + ), + selectBarWidthDays: createSelector( + (sliceState: History<GraphState>) => sliceState.current.barDuration, + durationString => { + return moment.duration(durationString); + } + ), + selectMapBarWidthDays: createSelector( + (sliceState: History<GraphState>) => sliceState.current.mapsBarDuration, + durationString => { + return moment.duration(durationString); + } + ) } }); @@ -456,6 +491,7 @@ export const historyStepForward = createAction('graph/historyStepForward'); export const updateHistory = createAction<GraphState>('graph/updateHistory'); export const processGraphLink = createAction<URLSearchParams>('graph/graphLink'); export const clearGraphHistory = createAction('graph/clearHistory'); -export const updateSliderRange = createAction<TimeInterval>('graph/UpdateSliderRange'); +export const updateSliderRange = createAction<string>('graph/UpdateSliderRange'); +export const setGraphSliceState = createAction<History<GraphState>>('graph/SetGraphSliceState'); diff --git a/src/client/app/redux/slices/localEditsSlice.ts b/src/client/app/redux/slices/localEditsSlice.ts index bfb4ae48b..2696eac44 100644 --- a/src/client/app/redux/slices/localEditsSlice.ts +++ b/src/client/app/redux/slices/localEditsSlice.ts @@ -2,11 +2,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { createEntityAdapter } from '@reduxjs/toolkit'; -import { PlotMouseEvent } from 'plotly.js'; +import { mapsAdapter } from '../../redux/entityAdapters'; import { createThunkSlice } from '../../redux/sliceCreators'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; import { calibrate, CalibratedPoint, CartesianPoint, GPSPoint } from '../../utils/calibration'; -import { mapsAdapter } from '../../redux/entityAdapters'; const localEditAdapter = createEntityAdapter<MapMetadata>(); const localSelectors = localEditAdapter.getSelectors(); @@ -65,30 +64,10 @@ export const localEditsSlice = createThunkSlice({ } } }), - updateCurrentCartesian: create.reducer<PlotMouseEvent>((state, { payload }) => { - // repourposed getClickedCoordinate Events from previous maps implementatinon moved to reducer - // trace 0 keeps a transparent trace of closely positioned points used for calibration(backgroundTrace), - // trace 1 keeps the data points used for calibration are automatically added to the same trace(dataPointTrace), - // event.points will include all points near a mouse click, including those in the backgroundTrace and the dataPointTrace, - // so the algorithm only looks at trace 0 since points from trace 1 are already put into the data set used for calibration. - const eligiblePoints = []; - for (const point of payload.points) { - const traceNumber = point.curveNumber; - if (traceNumber === 0) { - eligiblePoints.push(point); - } - } - // TODO VERIFY - const xValue = eligiblePoints[0].x as number; - const yValue = eligiblePoints[0].y as number; - const clickedPoint: CartesianPoint = { - x: Number(xValue.toFixed(6)), - y: Number(yValue.toFixed(6)) - }; - + updateCurrentCartesian: create.reducer<CartesianPoint>((state, { payload }) => { // update calibrating map with new datapoint const currentPoint: CalibratedPoint = { - cartesian: clickedPoint, + cartesian: payload, gps: { longitude: -1, latitude: -1 } }; diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index 8640761ac..83cc9f307 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -2,8 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as moment from 'moment'; -import { TimeInterval } from '../../../../common/TimeInterval'; import { ComparePeriod, SortingOrder } from '../../utils/calculateCompare'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; @@ -62,17 +60,20 @@ export interface GraphState { selectedUnit: number; selectedMap: number; selectedAreaUnit: AreaUnitType; - rangeSliderInterval: TimeInterval; - barDuration: moment.Duration; - mapsBarDuration: moment.Duration; comparePeriod: ComparePeriod; - compareTimeInterval: TimeInterval; compareSortingOrder: SortingOrder; chartToRender: ChartTypes; barStacking: boolean; lineGraphRate: LineGraphRate; showMinMax: boolean; threeD: ThreeDState; - queryTimeInterval: TimeInterval; hotlinked: boolean; + // save time intervals as strings. + // convert to TimeInterval w/ TimeInterval.fromString() + rangeSliderIntervalString: string; + compareTimeIntervalString: string; + queryTimeIntervalString: string; + // save duration as string + barDuration: string; + mapsBarDuration: string; }